mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 19:07:04 +01:00
linux_configuration: track ~/.local/bin wrapper scripts in repo, symlink instead of generating
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (mypy, mypy) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (pylint, pylint) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (rest, ) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (shellcheck, shellcheck) (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / lib-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (mypy, mypy) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (pylint, pylint) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (rest, ) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (shellcheck, shellcheck) (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / lib-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Moves 7 loose ~/.local/bin scripts into linux_configuration/scripts/ and tracks the flutter/dart git-env wrapper; fix_flutter_git_env.sh and unreal_mcp_repo.sh now symlink to the tracked files instead of writing heredocs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014yhTkYV6ZeuAbQYvykFGFc Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f8a7aded7b
commit
8be1fac8f9
@ -0,0 +1,14 @@
|
||||
{
|
||||
"title": "Track hand-placed ~/.local/bin scripts in linux_configuration/scripts",
|
||||
"objective": "Every hand-placed script in ~/.local/bin (fix-execstack-launch.sh, nvidia-pmon-logger.sh, usage-report-catchup.sh, krita, blender-mcp-host, blender-mcp-serve, unreal-mcp-server) becomes a tracked file under linux_configuration/scripts/, and ~/.local/bin keeps a symlink with the original name so every caller (systemd user units, ~/.claude.json MCP config) still works. install_unreal_mcp.sh symlinks the launcher instead of generating it with a heredoc.",
|
||||
"acceptance_criteria": [
|
||||
"readlink -f ~/.local/bin/<name> resolves to linux_configuration/scripts/<file> for all seven names (krita -> krita-scaled.sh)",
|
||||
"bash -n passes for every tracked script; git ls-files -s shows mode 100755",
|
||||
"install_launcher creates ~/.local/bin/unreal-mcp-server as a symlink to the tracked wrapper, idempotently, and chowns the link (not the target) under sudo"
|
||||
],
|
||||
"out_of_scope": [
|
||||
"Editing systemd units, crontab or MCP configs (symlinks keep their paths valid)",
|
||||
"Changing the moved scripts' behaviour"
|
||||
],
|
||||
"verifier": "readlink -f + bash -n loop over ~/.local/bin entries; install_launcher run against a temp USER_HOME; pre-commit hooks on the staged files"
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
{
|
||||
"intent": "Hand-placed ~/.local/bin scripts are version-controlled in linux_configuration/scripts/ while every existing ~/.local/bin path keeps working through a symlink.",
|
||||
"scope": [
|
||||
"linux_configuration/scripts/{fix-execstack-launch.sh,nvidia-pmon-logger.sh,usage-report-catchup.sh,krita-scaled.sh,blender-mcp-host,blender-mcp-serve,unreal-mcp-server}, linux_configuration/features/lib/unreal_mcp_repo.sh, linux_configuration/features/install_unreal_mcp.sh",
|
||||
"Non-goal: no behaviour change to the moved scripts; callers are not edited"
|
||||
],
|
||||
"changes": [
|
||||
"Six scripts copied byte-for-byte (cmp-verified, mode preserved); ~/.local/bin/krita is now named krita-scaled.sh in the repo",
|
||||
"unreal-mcp-server is tracked; it reads UNREAL_MCP_PYTHON_DIR (default ~/.local/share/unreal-mcp/unreal-mcp/Python) because a static file cannot bake in --install-dir",
|
||||
"install_launcher replaces the heredoc with ln -sfn to the tracked wrapper, chown -h under sudo, and logs a warning to export UNREAL_MCP_PYTHON_DIR when --install-dir is non-default"
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "for n in ...; do readlink -f ~/.local/bin/$n; bash -n ~/.local/bin/$n; done",
|
||||
"result": "pass",
|
||||
"evidence": "all seven names resolve into linux_configuration/scripts and parse"
|
||||
},
|
||||
{
|
||||
"command": "source lib/unreal_mcp_repo.sh with USER_HOME=<tmpdir>; install_launcher twice over a stale regular file",
|
||||
"result": "pass",
|
||||
"evidence": "regular file replaced by symlink to scripts/unreal-mcp-server; second run idempotent; non-default install dir warning logged"
|
||||
},
|
||||
{
|
||||
"command": "pre-commit run <shellcheck|no-inline-python|no-polling-antipatterns|shell-coverage-ratchet|file-length-cap> --files <staged>",
|
||||
"result": "pass",
|
||||
"evidence": "all passed"
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"A non-default --install-dir now needs UNREAL_MCP_PYTHON_DIR exported where the MCP client launches the server",
|
||||
"Moving or renaming the repo checkout breaks the ~/.local/bin symlinks"
|
||||
],
|
||||
"rollback": [
|
||||
"git revert the commit, then cp the scripts from the previous ~/.local/bin state (git show <hash>:linux_configuration/scripts/<file>) over the symlinks",
|
||||
"Check systemctl --user status nvidia-pmon.service usage-report-catchup.timer afterwards"
|
||||
]
|
||||
}
|
||||
@ -2,7 +2,7 @@
|
||||
# Install Unreal MCP and connect it to VS Code (via Continue MCP) on Arch Linux
|
||||
# - Installs deps: git, jq, uv, python
|
||||
# - Clones https://github.com/chongdashu/unreal-mcp
|
||||
# - Creates a launcher: ~/.local/bin/unreal-mcp-server
|
||||
# - Symlinks ~/.local/bin/unreal-mcp-server to the tracked scripts/unreal-mcp-server
|
||||
# - Configures VS Code Continue MCP: ~/.continue/config.json
|
||||
# - Optional: copies UnrealMCP plugin into a specified .uproject's Plugins/
|
||||
|
||||
@ -31,12 +31,12 @@ FORCE_UPDATE=false
|
||||
|
||||
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
|
||||
fail() {
|
||||
echo "[ERROR] $*" >&2
|
||||
exit 1
|
||||
echo "[ERROR] $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat << EOF
|
||||
cat <<EOF
|
||||
Usage: $SCRIPT_NAME [options]
|
||||
|
||||
Options:
|
||||
@ -55,101 +55,101 @@ EOF
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--install-dir)
|
||||
shift
|
||||
[[ $# -gt 0 ]] || fail "--install-dir requires a value"
|
||||
INSTALL_ROOT="$1"
|
||||
;;
|
||||
--project)
|
||||
shift
|
||||
[[ $# -gt 0 ]] || fail "--project requires a path to .uproject"
|
||||
PROJECT_UPROJECT="$1"
|
||||
;;
|
||||
--no-continue)
|
||||
CONFIGURE_CONTINUE=false
|
||||
;;
|
||||
--no-vscode)
|
||||
CONFIGURE_VSCODE_USER=false
|
||||
;;
|
||||
--force-update)
|
||||
FORCE_UPDATE=true
|
||||
;;
|
||||
-h | --help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
fail "Unknown option: $1"
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
case "$1" in
|
||||
--install-dir)
|
||||
shift
|
||||
[[ $# -gt 0 ]] || fail "--install-dir requires a value"
|
||||
INSTALL_ROOT="$1"
|
||||
;;
|
||||
--project)
|
||||
shift
|
||||
[[ $# -gt 0 ]] || fail "--project requires a path to .uproject"
|
||||
PROJECT_UPROJECT="$1"
|
||||
;;
|
||||
--no-continue)
|
||||
CONFIGURE_CONTINUE=false
|
||||
;;
|
||||
--no-vscode)
|
||||
CONFIGURE_VSCODE_USER=false
|
||||
;;
|
||||
--force-update)
|
||||
FORCE_UPDATE=true
|
||||
;;
|
||||
-h | --help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
fail "Unknown option: $1"
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
REPO_DIR="$INSTALL_ROOT/unreal-mcp"
|
||||
|
||||
# ---------- Dependencies ----------
|
||||
require_cmd() { command -v "$1" > /dev/null 2>&1; }
|
||||
require_cmd() { command -v "$1" >/dev/null 2>&1; }
|
||||
|
||||
ensure_packages_arch() {
|
||||
# Install with pacman using sudo when needed; keep idempotent with --needed
|
||||
local pkgs=(git jq uv python rsync)
|
||||
local to_install=()
|
||||
for p in "${pkgs[@]}"; do
|
||||
if ! pacman -Qi "$p" > /dev/null 2>&1; then
|
||||
to_install+=("$p")
|
||||
fi
|
||||
done
|
||||
if [[ ${#to_install[@]} -gt 0 ]]; then
|
||||
log "Installing packages: ${to_install[*]}"
|
||||
if [[ $EUID -eq 0 ]]; then
|
||||
pacman -S --noconfirm --needed "${to_install[@]}"
|
||||
else
|
||||
sudo pacman -S --noconfirm --needed "${to_install[@]}"
|
||||
fi
|
||||
else
|
||||
log "All required packages already installed"
|
||||
fi
|
||||
# Install with pacman using sudo when needed; keep idempotent with --needed
|
||||
local pkgs=(git jq uv python rsync)
|
||||
local to_install=()
|
||||
for p in "${pkgs[@]}"; do
|
||||
if ! pacman -Qi "$p" >/dev/null 2>&1; then
|
||||
to_install+=("$p")
|
||||
fi
|
||||
done
|
||||
if [[ ${#to_install[@]} -gt 0 ]]; then
|
||||
log "Installing packages: ${to_install[*]}"
|
||||
if [[ $EUID -eq 0 ]]; then
|
||||
pacman -S --noconfirm --needed "${to_install[@]}"
|
||||
else
|
||||
sudo pacman -S --noconfirm --needed "${to_install[@]}"
|
||||
fi
|
||||
else
|
||||
log "All required packages already installed"
|
||||
fi
|
||||
}
|
||||
|
||||
check_python_version() {
|
||||
if require_cmd python; then
|
||||
local v
|
||||
v=$(python -V 2>&1 | awk '{print $2}')
|
||||
elif require_cmd python3; then
|
||||
local v
|
||||
v=$(python3 -V 2>&1 | awk '{print $2}')
|
||||
else
|
||||
log "python not found; pacman install will provide it"
|
||||
return 0
|
||||
fi
|
||||
# Require >= 3.12 (Unreal MCP docs)
|
||||
local major minor
|
||||
major=$(echo "$v" | cut -d. -f1)
|
||||
minor=$(echo "$v" | cut -d. -f2)
|
||||
if ((major < 3 || (major == 3 && minor < 12))); then
|
||||
log "Python $v detected; installing newer python via pacman"
|
||||
if [[ $EUID -eq 0 ]]; then
|
||||
pacman -S --noconfirm --needed python
|
||||
else
|
||||
sudo pacman -S --noconfirm --needed python
|
||||
fi
|
||||
fi
|
||||
if require_cmd python; then
|
||||
local v
|
||||
v=$(python -V 2>&1 | awk '{print $2}')
|
||||
elif require_cmd python3; then
|
||||
local v
|
||||
v=$(python3 -V 2>&1 | awk '{print $2}')
|
||||
else
|
||||
log "python not found; pacman install will provide it"
|
||||
return 0
|
||||
fi
|
||||
# Require >= 3.12 (Unreal MCP docs)
|
||||
local major minor
|
||||
major=$(echo "$v" | cut -d. -f1)
|
||||
minor=$(echo "$v" | cut -d. -f2)
|
||||
if ((major < 3 || (major == 3 && minor < 12))); then
|
||||
log "Python $v detected; installing newer python via pacman"
|
||||
if [[ $EUID -eq 0 ]]; then
|
||||
pacman -S --noconfirm --needed python
|
||||
else
|
||||
sudo pacman -S --noconfirm --needed python
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# shellcheck source=lib/unreal_mcp_repo.sh
|
||||
source "$SCRIPT_DIR/lib/unreal_mcp_repo.sh"
|
||||
|
||||
main() {
|
||||
log "Installing prerequisites (Arch Linux)"
|
||||
ensure_packages_arch
|
||||
check_python_version
|
||||
setup_repo
|
||||
install_launcher
|
||||
configure_continue
|
||||
install_plugin_into_project
|
||||
configure_vscode_user_mcp
|
||||
print_summary
|
||||
log "Installing prerequisites (Arch Linux)"
|
||||
ensure_packages_arch
|
||||
check_python_version
|
||||
setup_repo
|
||||
install_launcher
|
||||
configure_continue
|
||||
install_plugin_into_project
|
||||
configure_vscode_user_mcp
|
||||
print_summary
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
@ -7,66 +7,67 @@
|
||||
|
||||
# ---------- Git clone/update ----------
|
||||
setup_repo() {
|
||||
mkdir -p "$INSTALL_ROOT"
|
||||
if [[ ! -d "$REPO_DIR/.git" ]]; then
|
||||
log "Cloning unreal-mcp into $REPO_DIR"
|
||||
if require_cmd git; then
|
||||
git clone "$REPO_URL" "$REPO_DIR"
|
||||
else
|
||||
fail "git is required but not found after install"
|
||||
fi
|
||||
else
|
||||
log "Repo exists at $REPO_DIR"
|
||||
if [[ $FORCE_UPDATE == true ]]; then
|
||||
log "Updating repo with --force-update"
|
||||
git -C "$REPO_DIR" fetch origin
|
||||
git -C "$REPO_DIR" reset --hard origin/main
|
||||
git -C "$REPO_DIR" pull --rebase --autostash
|
||||
else
|
||||
log "Pulling latest changes"
|
||||
git -C "$REPO_DIR" pull --rebase --autostash
|
||||
fi
|
||||
fi
|
||||
mkdir -p "$INSTALL_ROOT"
|
||||
if [[ ! -d "$REPO_DIR/.git" ]]; then
|
||||
log "Cloning unreal-mcp into $REPO_DIR"
|
||||
if require_cmd git; then
|
||||
git clone "$REPO_URL" "$REPO_DIR"
|
||||
else
|
||||
fail "git is required but not found after install"
|
||||
fi
|
||||
else
|
||||
log "Repo exists at $REPO_DIR"
|
||||
if [[ $FORCE_UPDATE == true ]]; then
|
||||
log "Updating repo with --force-update"
|
||||
git -C "$REPO_DIR" fetch origin
|
||||
git -C "$REPO_DIR" reset --hard origin/main
|
||||
git -C "$REPO_DIR" pull --rebase --autostash
|
||||
else
|
||||
log "Pulling latest changes"
|
||||
git -C "$REPO_DIR" pull --rebase --autostash
|
||||
fi
|
||||
fi
|
||||
|
||||
# Ensure ownership for the real user when script ran via sudo
|
||||
if [[ $EUID -eq 0 ]]; then
|
||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$INSTALL_ROOT"
|
||||
fi
|
||||
# Ensure ownership for the real user when script ran via sudo
|
||||
if [[ $EUID -eq 0 ]]; then
|
||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$INSTALL_ROOT"
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------- Launcher ----------
|
||||
install_launcher() {
|
||||
local bin_dir="$USER_HOME/.local/bin"
|
||||
local python_dir="$REPO_DIR/Python"
|
||||
local launcher="$bin_dir/unreal-mcp-server"
|
||||
mkdir -p "$bin_dir"
|
||||
cat > "$launcher" << EOF
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
exec uv --directory "$python_dir" run unreal_mcp_server.py "\${1:-}" < /dev/null
|
||||
EOF
|
||||
chmod +x "$launcher"
|
||||
if [[ $EUID -eq 0 ]]; then chown "$ACTUAL_USER:$ACTUAL_USER" "$launcher"; fi
|
||||
log "Installed launcher: $launcher"
|
||||
local bin_dir="$USER_HOME/.local/bin"
|
||||
local launcher="$bin_dir/unreal-mcp-server"
|
||||
# The wrapper is tracked in the repo; ~/.local/bin only holds a symlink.
|
||||
local target
|
||||
target="$(readlink -f "$SCRIPT_DIR/../scripts/unreal-mcp-server")"
|
||||
[[ -x $target ]] || fail "Tracked launcher missing: $target"
|
||||
mkdir -p "$bin_dir"
|
||||
ln -sfn "$target" "$launcher"
|
||||
if [[ $EUID -eq 0 ]]; then chown -h "$ACTUAL_USER:$ACTUAL_USER" "$launcher"; fi
|
||||
if [[ $INSTALL_ROOT != "$INSTALL_ROOT_DEFAULT" ]]; then
|
||||
log "WARNING: non-default --install-dir; export UNREAL_MCP_PYTHON_DIR=$REPO_DIR/Python for the launcher"
|
||||
fi
|
||||
log "Installed launcher: $launcher -> $target"
|
||||
}
|
||||
|
||||
# ---------- VS Code: Continue MCP config ----------
|
||||
configure_continue() {
|
||||
if [[ $CONFIGURE_CONTINUE != true ]]; then
|
||||
log "Skipping Continue config (--no-continue)"
|
||||
return 0
|
||||
fi
|
||||
if [[ $CONFIGURE_CONTINUE != true ]]; then
|
||||
log "Skipping Continue config (--no-continue)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local cont_dir="$USER_HOME/.continue"
|
||||
local cont_cfg="$cont_dir/config.json"
|
||||
local python_dir="$REPO_DIR/Python"
|
||||
mkdir -p "$cont_dir"
|
||||
local cont_dir="$USER_HOME/.continue"
|
||||
local cont_cfg="$cont_dir/config.json"
|
||||
local python_dir="$REPO_DIR/Python"
|
||||
mkdir -p "$cont_dir"
|
||||
|
||||
# Base JSON when no config exists
|
||||
local tmp_file
|
||||
tmp_file="$(mktemp)"
|
||||
if [[ ! -f $cont_cfg ]]; then
|
||||
cat > "$tmp_file" << JSON
|
||||
# Base JSON when no config exists
|
||||
local tmp_file
|
||||
tmp_file="$(mktemp)"
|
||||
if [[ ! -f $cont_cfg ]]; then
|
||||
cat >"$tmp_file" <<JSON
|
||||
{
|
||||
"mcpServers": {
|
||||
"unrealMCP": {
|
||||
@ -76,147 +77,147 @@ configure_continue() {
|
||||
}
|
||||
}
|
||||
JSON
|
||||
mv "$tmp_file" "$cont_cfg"
|
||||
else
|
||||
# Merge using jq: ensure .mcpServers exists, then set/overwrite unrealMCP
|
||||
if ! require_cmd jq; then
|
||||
fail "jq is required to merge ~/.continue/config.json"
|
||||
fi
|
||||
jq --arg dir "$python_dir" '
|
||||
mv "$tmp_file" "$cont_cfg"
|
||||
else
|
||||
# Merge using jq: ensure .mcpServers exists, then set/overwrite unrealMCP
|
||||
if ! require_cmd jq; then
|
||||
fail "jq is required to merge ~/.continue/config.json"
|
||||
fi
|
||||
jq --arg dir "$python_dir" '
|
||||
.mcpServers = (.mcpServers // {}) |
|
||||
.mcpServers.unrealMCP = {
|
||||
command: "uv",
|
||||
args: ["--directory", $dir, "run", "unreal_mcp_server.py"]
|
||||
}
|
||||
' "$cont_cfg" > "$tmp_file" && mv "$tmp_file" "$cont_cfg"
|
||||
fi
|
||||
' "$cont_cfg" >"$tmp_file" && mv "$tmp_file" "$cont_cfg"
|
||||
fi
|
||||
|
||||
if [[ $EUID -eq 0 ]]; then chown "$ACTUAL_USER:$ACTUAL_USER" "$cont_cfg"; fi
|
||||
log "Configured Continue MCP at: $cont_cfg"
|
||||
if [[ $EUID -eq 0 ]]; then chown "$ACTUAL_USER:$ACTUAL_USER" "$cont_cfg"; fi
|
||||
log "Configured Continue MCP at: $cont_cfg"
|
||||
}
|
||||
|
||||
# ---------- VS Code user MCP (native) ----------
|
||||
configure_vscode_user_mcp() {
|
||||
if [[ $CONFIGURE_VSCODE_USER != true ]]; then
|
||||
log "Skipping VS Code user MCP config (--no-vscode)"
|
||||
return 0
|
||||
fi
|
||||
if [[ $CONFIGURE_VSCODE_USER != true ]]; then
|
||||
log "Skipping VS Code user MCP config (--no-vscode)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! require_cmd jq; then
|
||||
fail "jq is required to compose VS Code --add-mcp JSON and to parse profiles"
|
||||
fi
|
||||
if ! require_cmd jq; then
|
||||
fail "jq is required to compose VS Code --add-mcp JSON and to parse profiles"
|
||||
fi
|
||||
|
||||
local python_dir="$REPO_DIR/Python"
|
||||
local json
|
||||
json=$(jq -n --arg dir "$python_dir" '{name:"unrealMCP", command:"uv", args:["--directory", $dir, "run", "unreal_mcp_server.py"]}')
|
||||
local python_dir="$REPO_DIR/Python"
|
||||
local json
|
||||
json=$(jq -n --arg dir "$python_dir" '{name:"unrealMCP", command:"uv", args:["--directory", $dir, "run", "unreal_mcp_server.py"]}')
|
||||
|
||||
# Handle multiple VS Code variants if present
|
||||
local candidates=(code code-insiders codium)
|
||||
local found_any=false
|
||||
for cli in "${candidates[@]}"; do
|
||||
if ! command -v "$cli" > /dev/null 2>&1; then
|
||||
continue
|
||||
fi
|
||||
found_any=true
|
||||
log "Registering MCP server in VS Code user profile via: $cli --add-mcp"
|
||||
if "$cli" --add-mcp "$json" > "/tmp/${cli}-add-mcp.log" 2>&1; then
|
||||
log "[$cli] user profile: unrealMCP added/updated"
|
||||
else
|
||||
sed -n '1,200p' "/tmp/${cli}-add-mcp.log" || true
|
||||
fail "[$cli] --add-mcp failed for user profile. Ensure your VS Code supports MCP or rerun with --no-vscode."
|
||||
fi
|
||||
# Handle multiple VS Code variants if present
|
||||
local candidates=(code code-insiders codium)
|
||||
local found_any=false
|
||||
for cli in "${candidates[@]}"; do
|
||||
if ! command -v "$cli" >/dev/null 2>&1; then
|
||||
continue
|
||||
fi
|
||||
found_any=true
|
||||
log "Registering MCP server in VS Code user profile via: $cli --add-mcp"
|
||||
if "$cli" --add-mcp "$json" >"/tmp/${cli}-add-mcp.log" 2>&1; then
|
||||
log "[$cli] user profile: unrealMCP added/updated"
|
||||
else
|
||||
sed -n '1,200p' "/tmp/${cli}-add-mcp.log" || true
|
||||
fail "[$cli] --add-mcp failed for user profile. Ensure your VS Code supports MCP or rerun with --no-vscode."
|
||||
fi
|
||||
|
||||
# Detect profiles with 'unreal' (case-insensitive) and add there too
|
||||
local data_dir=""
|
||||
case "$cli" in
|
||||
code)
|
||||
data_dir="$USER_HOME/.config/Code"
|
||||
;;
|
||||
code-insiders)
|
||||
data_dir="$USER_HOME/.config/Code - Insiders"
|
||||
;;
|
||||
codium)
|
||||
data_dir="$USER_HOME/.config/VSCodium"
|
||||
;;
|
||||
esac
|
||||
local profiles_json="$data_dir/User/profiles/profiles.json"
|
||||
if [[ -f $profiles_json ]]; then
|
||||
# Extract profile names matching /unreal/i
|
||||
mapfile -t unreal_profiles < <(jq -r '.profiles // [] | .[] | .name // empty | select(test("unreal"; "i"))' "$profiles_json")
|
||||
if [[ ${#unreal_profiles[@]} -gt 0 ]]; then
|
||||
log "[$cli] Found profiles with 'unreal': ${unreal_profiles[*]}"
|
||||
local name
|
||||
for name in "${unreal_profiles[@]}"; do
|
||||
log "[$cli] Adding unrealMCP to profile: $name"
|
||||
if "$cli" --profile "$name" --add-mcp "$json" > "/tmp/${cli}-add-mcp-${name// /_}.log" 2>&1; then
|
||||
log "[$cli] profile '$name': unrealMCP added/updated"
|
||||
else
|
||||
sed -n '1,200p' "/tmp/${cli}-add-mcp-${name// /_}.log" || true
|
||||
fail "[$cli] --add-mcp failed for profile '$name'."
|
||||
fi
|
||||
done
|
||||
else
|
||||
log "[$cli] No VS Code profiles with 'unreal' in name"
|
||||
fi
|
||||
else
|
||||
log "[$cli] Profiles file not found: $profiles_json (skipping profile-specific adds)"
|
||||
fi
|
||||
done
|
||||
# Detect profiles with 'unreal' (case-insensitive) and add there too
|
||||
local data_dir=""
|
||||
case "$cli" in
|
||||
code)
|
||||
data_dir="$USER_HOME/.config/Code"
|
||||
;;
|
||||
code-insiders)
|
||||
data_dir="$USER_HOME/.config/Code - Insiders"
|
||||
;;
|
||||
codium)
|
||||
data_dir="$USER_HOME/.config/VSCodium"
|
||||
;;
|
||||
esac
|
||||
local profiles_json="$data_dir/User/profiles/profiles.json"
|
||||
if [[ -f $profiles_json ]]; then
|
||||
# Extract profile names matching /unreal/i
|
||||
mapfile -t unreal_profiles < <(jq -r '.profiles // [] | .[] | .name // empty | select(test("unreal"; "i"))' "$profiles_json")
|
||||
if [[ ${#unreal_profiles[@]} -gt 0 ]]; then
|
||||
log "[$cli] Found profiles with 'unreal': ${unreal_profiles[*]}"
|
||||
local name
|
||||
for name in "${unreal_profiles[@]}"; do
|
||||
log "[$cli] Adding unrealMCP to profile: $name"
|
||||
if "$cli" --profile "$name" --add-mcp "$json" >"/tmp/${cli}-add-mcp-${name// /_}.log" 2>&1; then
|
||||
log "[$cli] profile '$name': unrealMCP added/updated"
|
||||
else
|
||||
sed -n '1,200p' "/tmp/${cli}-add-mcp-${name// /_}.log" || true
|
||||
fail "[$cli] --add-mcp failed for profile '$name'."
|
||||
fi
|
||||
done
|
||||
else
|
||||
log "[$cli] No VS Code profiles with 'unreal' in name"
|
||||
fi
|
||||
else
|
||||
log "[$cli] Profiles file not found: $profiles_json (skipping profile-specific adds)"
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $found_any == false ]]; then
|
||||
fail "VS Code CLI not found (code/code-insiders/codium). Install VS Code and ensure 'code' CLI is available, or run with --no-vscode to skip."
|
||||
fi
|
||||
if [[ $found_any == false ]]; then
|
||||
fail "VS Code CLI not found (code/code-insiders/codium). Install VS Code and ensure 'code' CLI is available, or run with --no-vscode to skip."
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------- Unreal Plugin copy (optional) ----------
|
||||
install_plugin_into_project() {
|
||||
[[ -n $PROJECT_UPROJECT ]] || return 0
|
||||
local upath="$PROJECT_UPROJECT"
|
||||
if [[ -d $upath ]]; then
|
||||
# Resolve .uproject in the provided directory
|
||||
mapfile -t _uprojects < <(find "$upath" -maxdepth 1 -type f -name "*.uproject" 2> /dev/null || true)
|
||||
if [[ ${#_uprojects[@]} -eq 0 ]]; then
|
||||
fail "--project directory '$upath' contains no .uproject files"
|
||||
elif [[ ${#_uprojects[@]} -gt 1 ]]; then
|
||||
printf '[ERROR] Multiple .uproject files found in %s:\n' "$upath" >&2
|
||||
printf ' - %s\n' "${_uprojects[@]}" >&2
|
||||
fail "Please pass the specific .uproject path to --project"
|
||||
else
|
||||
upath="${_uprojects[0]}"
|
||||
log "Resolved .uproject: $upath"
|
||||
fi
|
||||
elif [[ -f $upath ]]; then
|
||||
true
|
||||
else
|
||||
fail "--project path does not exist: $upath"
|
||||
fi
|
||||
if [[ ${upath##*.} != "uproject" ]]; then
|
||||
fail "--project must point to a .uproject file (got: $upath)"
|
||||
fi
|
||||
local proj_dir
|
||||
proj_dir="$(cd "$(dirname "$upath")" && pwd)"
|
||||
RESOLVED_PROJECT_DIR="$proj_dir"
|
||||
local src_plugin="$REPO_DIR/MCPGameProject/Plugins/UnrealMCP"
|
||||
local dst_plugin="$proj_dir/Plugins/UnrealMCP"
|
||||
if [[ ! -d $src_plugin ]]; then
|
||||
fail "Source plugin not found at $src_plugin (did repo layout change?)"
|
||||
fi
|
||||
mkdir -p "$proj_dir/Plugins"
|
||||
log "Copying UnrealMCP plugin to project: $dst_plugin"
|
||||
rsync -a --delete "$src_plugin/" "$dst_plugin/"
|
||||
# Set ownership back to actual user if run as root
|
||||
if [[ $EUID -eq 0 ]]; then chown -R "$ACTUAL_USER:$ACTUAL_USER" "$proj_dir/Plugins"; fi
|
||||
log "Plugin installed. Enable it from Unreal Editor (Edit > Plugins) if needed."
|
||||
[[ -n $PROJECT_UPROJECT ]] || return 0
|
||||
local upath="$PROJECT_UPROJECT"
|
||||
if [[ -d $upath ]]; then
|
||||
# Resolve .uproject in the provided directory
|
||||
mapfile -t _uprojects < <(find "$upath" -maxdepth 1 -type f -name "*.uproject" 2>/dev/null || true)
|
||||
if [[ ${#_uprojects[@]} -eq 0 ]]; then
|
||||
fail "--project directory '$upath' contains no .uproject files"
|
||||
elif [[ ${#_uprojects[@]} -gt 1 ]]; then
|
||||
printf '[ERROR] Multiple .uproject files found in %s:\n' "$upath" >&2
|
||||
printf ' - %s\n' "${_uprojects[@]}" >&2
|
||||
fail "Please pass the specific .uproject path to --project"
|
||||
else
|
||||
upath="${_uprojects[0]}"
|
||||
log "Resolved .uproject: $upath"
|
||||
fi
|
||||
elif [[ -f $upath ]]; then
|
||||
true
|
||||
else
|
||||
fail "--project path does not exist: $upath"
|
||||
fi
|
||||
if [[ ${upath##*.} != "uproject" ]]; then
|
||||
fail "--project must point to a .uproject file (got: $upath)"
|
||||
fi
|
||||
local proj_dir
|
||||
proj_dir="$(cd "$(dirname "$upath")" && pwd)"
|
||||
RESOLVED_PROJECT_DIR="$proj_dir"
|
||||
local src_plugin="$REPO_DIR/MCPGameProject/Plugins/UnrealMCP"
|
||||
local dst_plugin="$proj_dir/Plugins/UnrealMCP"
|
||||
if [[ ! -d $src_plugin ]]; then
|
||||
fail "Source plugin not found at $src_plugin (did repo layout change?)"
|
||||
fi
|
||||
mkdir -p "$proj_dir/Plugins"
|
||||
log "Copying UnrealMCP plugin to project: $dst_plugin"
|
||||
rsync -a --delete "$src_plugin/" "$dst_plugin/"
|
||||
# Set ownership back to actual user if run as root
|
||||
if [[ $EUID -eq 0 ]]; then chown -R "$ACTUAL_USER:$ACTUAL_USER" "$proj_dir/Plugins"; fi
|
||||
log "Plugin installed. Enable it from Unreal Editor (Edit > Plugins) if needed."
|
||||
}
|
||||
|
||||
# ---------- Summary ----------
|
||||
print_summary() {
|
||||
local python_dir="$REPO_DIR/Python"
|
||||
local plugin_dest="N/A"
|
||||
if [[ -n $RESOLVED_PROJECT_DIR ]]; then
|
||||
plugin_dest="$RESOLVED_PROJECT_DIR/Plugins/UnrealMCP"
|
||||
fi
|
||||
cat << EOF
|
||||
local python_dir="$REPO_DIR/Python"
|
||||
local plugin_dest="N/A"
|
||||
if [[ -n $RESOLVED_PROJECT_DIR ]]; then
|
||||
plugin_dest="$RESOLVED_PROJECT_DIR/Plugins/UnrealMCP"
|
||||
fi
|
||||
cat <<EOF
|
||||
============================================
|
||||
Unreal MCP setup complete
|
||||
============================================
|
||||
|
||||
@ -41,143 +41,108 @@ set -euo pipefail
|
||||
SCRIPT_NAME="$(basename "$0")"
|
||||
readonly SCRIPT_NAME
|
||||
readonly BIN_DIR="${HOME}/.local/bin"
|
||||
readonly LIB_DIR="${HOME}/.local/share/flutter-git-env"
|
||||
readonly WRAPPER="${LIB_DIR}/wrapper.sh"
|
||||
SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "$0")")" && pwd)"
|
||||
readonly SCRIPT_DIR
|
||||
WRAPPER="$(readlink -f "${SCRIPT_DIR}/../scripts/flutter-git-env-wrapper.sh")"
|
||||
readonly WRAPPER
|
||||
readonly LEGACY_DIR="${HOME}/.local/share/flutter-git-env"
|
||||
# Every SDK entry point that shells out to git to identify itself.
|
||||
readonly TOOLS=(flutter dart)
|
||||
|
||||
log() { echo "$SCRIPT_NAME: $*"; }
|
||||
|
||||
fail() {
|
||||
echo "$SCRIPT_NAME: $*" >&2
|
||||
exit 1
|
||||
echo "$SCRIPT_NAME: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
write_wrapper() {
|
||||
mkdir -p "$LIB_DIR" "$BIN_DIR"
|
||||
cat > "$WRAPPER" <<'WRAPPER_EOF'
|
||||
#!/bin/bash
|
||||
|
||||
# Strip the git-hook environment, then hand off to the real SDK binary.
|
||||
# Installed by linux_configuration/fixes/fix_flutter_git_env.sh
|
||||
# -- see that script for why this exists. Invoked through a symlink named
|
||||
# after the tool it fronts (flutter, dart).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
tool="$(basename "$0")"
|
||||
real="/usr/bin/${tool}"
|
||||
|
||||
[[ -x $real ]] || {
|
||||
echo "flutter-git-env: $real is missing or not executable" >&2
|
||||
exit 127
|
||||
}
|
||||
|
||||
# Drop our own directory so a PATH lookup inside the real tool cannot find
|
||||
# this wrapper again. The Flutter entry point does exactly such a lookup.
|
||||
clean_path=""
|
||||
IFS=':' read -r -a path_parts <<< "$PATH"
|
||||
for part in "${path_parts[@]}"; do
|
||||
[[ $part == "$HOME/.local/bin" ]] && continue
|
||||
clean_path="${clean_path:+$clean_path:}$part"
|
||||
done
|
||||
|
||||
# GIT_DIR is the one that actually bites; the rest are exported by git in the
|
||||
# same breath and would mislead the SDK the same way.
|
||||
exec env \
|
||||
-u GIT_DIR \
|
||||
-u GIT_WORK_TREE \
|
||||
-u GIT_INDEX_FILE \
|
||||
-u GIT_COMMON_DIR \
|
||||
-u GIT_OBJECT_DIRECTORY \
|
||||
-u GIT_ALTERNATE_OBJECT_DIRECTORIES \
|
||||
-u GIT_NAMESPACE \
|
||||
-u GIT_PREFIX \
|
||||
PATH="$clean_path" \
|
||||
"$real" "$@"
|
||||
WRAPPER_EOF
|
||||
chmod +x "$WRAPPER"
|
||||
# The wrapper is tracked in the repo; ~/.local/bin only holds symlinks to it.
|
||||
# A copy left by an older install (LEGACY_DIR) is generated, so it is removed.
|
||||
check_wrapper() {
|
||||
[[ -x $WRAPPER ]] || fail "tracked wrapper missing: $WRAPPER"
|
||||
mkdir -p "$BIN_DIR"
|
||||
rm -rf "$LEGACY_DIR"
|
||||
}
|
||||
|
||||
link_tools() {
|
||||
local tool link
|
||||
for tool in "${TOOLS[@]}"; do
|
||||
link="${BIN_DIR}/${tool}"
|
||||
if [[ -e $link && ! -L $link ]]; then
|
||||
fail "$link exists and is not a symlink — refusing to overwrite it"
|
||||
fi
|
||||
ln -sfn "$WRAPPER" "$link"
|
||||
log "linked $link -> $WRAPPER"
|
||||
done
|
||||
local tool link
|
||||
for tool in "${TOOLS[@]}"; do
|
||||
link="${BIN_DIR}/${tool}"
|
||||
if [[ -e $link && ! -L $link ]]; then
|
||||
fail "$link exists and is not a symlink — refusing to overwrite it"
|
||||
fi
|
||||
ln -sfn "$WRAPPER" "$link"
|
||||
log "linked $link -> $WRAPPER"
|
||||
done
|
||||
}
|
||||
|
||||
check_path() {
|
||||
case ":$PATH:" in
|
||||
*":$BIN_DIR:"*) ;;
|
||||
*) fail "$BIN_DIR is not on PATH — the wrapper would never be used" ;;
|
||||
esac
|
||||
local resolved
|
||||
for tool in "${TOOLS[@]}"; do
|
||||
resolved="$(command -v "$tool" || true)"
|
||||
[[ $resolved == "${BIN_DIR}/${tool}" ]] ||
|
||||
fail "$tool resolves to '$resolved', not the wrapper — check PATH order"
|
||||
done
|
||||
case ":$PATH:" in
|
||||
*":$BIN_DIR:"*) ;;
|
||||
*) fail "$BIN_DIR is not on PATH — the wrapper would never be used" ;;
|
||||
esac
|
||||
local resolved
|
||||
for tool in "${TOOLS[@]}"; do
|
||||
resolved="$(command -v "$tool" || true)"
|
||||
[[ $resolved == "${BIN_DIR}/${tool}" ]] ||
|
||||
fail "$tool resolves to '$resolved', not the wrapper — check PATH order"
|
||||
done
|
||||
}
|
||||
|
||||
# The real test: does the SDK still know what it is with GIT_DIR poisoned?
|
||||
verify() {
|
||||
local probe_repo probe_version
|
||||
probe_repo="$(mktemp -d)"
|
||||
git -C "$probe_repo" init -q
|
||||
git -C "$probe_repo" -c user.email=x@y -c user.name=x commit -q --allow-empty -m probe
|
||||
local probe_repo probe_version
|
||||
probe_repo="$(mktemp -d)"
|
||||
git -C "$probe_repo" init -q
|
||||
git -C "$probe_repo" -c user.email=x@y -c user.name=x commit -q --allow-empty -m probe
|
||||
|
||||
probe_version="$(GIT_DIR="$probe_repo/.git" flutter --version 2>&1 | head -1 || true)"
|
||||
rm -rf "$probe_repo"
|
||||
probe_version="$(GIT_DIR="$probe_repo/.git" flutter --version 2>&1 | head -1 || true)"
|
||||
rm -rf "$probe_repo"
|
||||
|
||||
if grep -q '0\.0\.0-unknown' <<< "$probe_version"; then
|
||||
fail "still broken: flutter reports an unknown SDK under GIT_DIR"
|
||||
fi
|
||||
if ! grep -q 'flutter/flutter' <<< "$probe_version"; then
|
||||
fail "still broken: flutter reports '$probe_version' under GIT_DIR"
|
||||
fi
|
||||
log "verified: $probe_version"
|
||||
if grep -q '0\.0\.0-unknown' <<<"$probe_version"; then
|
||||
fail "still broken: flutter reports an unknown SDK under GIT_DIR"
|
||||
fi
|
||||
if ! grep -q 'flutter/flutter' <<<"$probe_version"; then
|
||||
fail "still broken: flutter reports '$probe_version' under GIT_DIR"
|
||||
fi
|
||||
log "verified: $probe_version"
|
||||
}
|
||||
|
||||
uninstall() {
|
||||
local tool link
|
||||
for tool in "${TOOLS[@]}"; do
|
||||
link="${BIN_DIR}/${tool}"
|
||||
if [[ -L $link && "$(readlink -f "$link")" == "$WRAPPER" ]]; then
|
||||
rm -f "$link"
|
||||
log "removed $link"
|
||||
fi
|
||||
done
|
||||
rm -rf "$LIB_DIR"
|
||||
log "uninstalled"
|
||||
local tool link
|
||||
for tool in "${TOOLS[@]}"; do
|
||||
link="${BIN_DIR}/${tool}"
|
||||
if [[ -L $link && "$(readlink -f "$link")" == "$WRAPPER" ]]; then
|
||||
rm -f "$link"
|
||||
log "removed $link"
|
||||
fi
|
||||
done
|
||||
rm -rf "$LEGACY_DIR"
|
||||
log "uninstalled"
|
||||
}
|
||||
|
||||
main() {
|
||||
case "${1:-}" in
|
||||
--uninstall)
|
||||
uninstall
|
||||
return
|
||||
;;
|
||||
--check)
|
||||
[[ -x $WRAPPER ]] || fail "wrapper is not installed"
|
||||
check_path
|
||||
verify
|
||||
return
|
||||
;;
|
||||
"") ;;
|
||||
*) fail "unknown option: $1 (expected --check or --uninstall)" ;;
|
||||
esac
|
||||
case "${1:-}" in
|
||||
--uninstall)
|
||||
uninstall
|
||||
return
|
||||
;;
|
||||
--check)
|
||||
[[ -x $WRAPPER ]] || fail "wrapper is not installed"
|
||||
check_path
|
||||
verify
|
||||
return
|
||||
;;
|
||||
"") ;;
|
||||
*) fail "unknown option: $1 (expected --check or --uninstall)" ;;
|
||||
esac
|
||||
|
||||
command -v flutter >/dev/null 2>&1 || fail "flutter is not on PATH"
|
||||
write_wrapper
|
||||
link_tools
|
||||
check_path
|
||||
verify
|
||||
log "done — flutter/dart now ignore an inherited GIT_DIR"
|
||||
command -v flutter >/dev/null 2>&1 || fail "flutter is not on PATH"
|
||||
check_wrapper
|
||||
link_tools
|
||||
check_path
|
||||
verify
|
||||
log "done — flutter/dart now ignore an inherited GIT_DIR"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
174
linux_configuration/scripts/blender-mcp-host
Executable file
174
linux_configuration/scripts/blender-mcp-host
Executable file
@ -0,0 +1,174 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ============================================================================
|
||||
# Start Blender as a headless MCP host for blender-mcp.
|
||||
#
|
||||
# The blender-mcp addon refuses to run under `blender --background`: its
|
||||
# command queue is pumped by bpy.app.timers, which never tick in background
|
||||
# mode, so requests would be accepted and then never execute. The addon's own
|
||||
# error message names the fix -- give Blender a display. Xvfb provides one
|
||||
# without putting a window on a real output (i3 focus stealing is an explicit
|
||||
# hazard here), and a real EEVEE render works fine against it.
|
||||
#
|
||||
# Known limitation: get_viewport_screenshot returns success but writes an
|
||||
# all-black PNG under Xvfb -- there is no GL viewport to capture. Use
|
||||
# bpy.ops.render.render(write_still=True) via execute_code instead; that is
|
||||
# verified working and is what the artgate 3D route needs anyway.
|
||||
# ============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_NAME="$(basename "$0")"
|
||||
readonly SCRIPT_NAME
|
||||
readonly ADDON_PATH="/home/kuhy/src/mcp-servers/servers/blender-mcp/addon.py"
|
||||
readonly START_SCRIPT="/home/kuhy/.local/share/blender-mcp-host/start_addon.py"
|
||||
readonly LOG_FILE="${XDG_STATE_HOME:-$HOME/.local/state}/blender-mcp-host.log"
|
||||
|
||||
PORT="${BLENDER_PORT:-9876}"
|
||||
|
||||
usage() {
|
||||
echo "Usage: $SCRIPT_NAME [start|stop|status|restart]"
|
||||
echo " start Launch headless Blender hosting the MCP socket (default)"
|
||||
echo " stop Terminate the hosted Blender instance"
|
||||
echo " status Report whether the socket is being served"
|
||||
echo " restart stop, then start"
|
||||
echo
|
||||
echo "Env: BLENDER_PORT (default 9876)"
|
||||
exit 0
|
||||
}
|
||||
|
||||
validate_requirements() {
|
||||
local missing=0
|
||||
for tool in blender xvfb-run; do
|
||||
if ! command -v "$tool" >/dev/null 2>&1; then
|
||||
echo "Error: required tool '$tool' not found in PATH" >&2
|
||||
missing=1
|
||||
fi
|
||||
done
|
||||
if [[ ! -f "$ADDON_PATH" ]]; then
|
||||
echo "Error: addon not found at $ADDON_PATH" >&2
|
||||
missing=1
|
||||
fi
|
||||
[[ $missing -eq 0 ]] || exit 1
|
||||
}
|
||||
|
||||
# True when something is listening on the MCP port.
|
||||
port_is_served() {
|
||||
ss -ltn 2>/dev/null | grep -q ":${PORT}\b"
|
||||
}
|
||||
|
||||
# PID of the Blender process holding the port, empty if none.
|
||||
host_pid() {
|
||||
host_pids | head -1
|
||||
}
|
||||
|
||||
# Every process belonging to a host instance: the Blender that serves the
|
||||
# socket plus the xvfb-run wrapper that owns its display. Stopping only the
|
||||
# Blender pid leaves the wrapper behind, which then keeps a stale instance
|
||||
# alive and makes the next `start` a silent no-op.
|
||||
host_pids() {
|
||||
pgrep -f "blender .*--python ${START_SCRIPT}" || true
|
||||
pgrep -f "xvfb-run .*--python ${START_SCRIPT}" || true
|
||||
}
|
||||
|
||||
do_start() {
|
||||
if port_is_served; then
|
||||
echo "Already serving on port ${PORT} (pid $(host_pid))"
|
||||
return 0
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$START_SCRIPT")" "$(dirname "$LOG_FILE")"
|
||||
|
||||
# The addon's register() auto-starts the server, so this script only needs
|
||||
# to load it and then block -- Blender must stay alive to pump the timers.
|
||||
cat > "$START_SCRIPT" <<'PY'
|
||||
"""Load the blender-mcp addon; Blender's event loop then serves the socket."""
|
||||
import importlib.util
|
||||
import sys
|
||||
|
||||
_SPEC = importlib.util.spec_from_file_location(
|
||||
"blendermcp_addon", "/home/kuhy/src/mcp-servers/servers/blender-mcp/addon.py")
|
||||
_MOD = importlib.util.module_from_spec(_SPEC)
|
||||
sys.modules["blendermcp_addon"] = _MOD
|
||||
_SPEC.loader.exec_module(_MOD)
|
||||
_MOD.register()
|
||||
print("blender-mcp-host: addon registered, serving", flush=True)
|
||||
PY
|
||||
|
||||
echo "Starting headless Blender on port ${PORT}..."
|
||||
BLENDER_PORT="$PORT" nohup xvfb-run -a \
|
||||
blender --factory-startup --python "$START_SCRIPT" \
|
||||
>> "$LOG_FILE" 2>&1 &
|
||||
disown
|
||||
|
||||
# Wait for the socket rather than sleeping a fixed guess.
|
||||
local waited=0
|
||||
while [[ $waited -lt 30 ]]; do
|
||||
if port_is_served; then
|
||||
echo "Serving on port ${PORT} (pid $(host_pid))"
|
||||
echo "Log: $LOG_FILE"
|
||||
return 0
|
||||
fi
|
||||
command sleep 1
|
||||
waited=$((waited + 1))
|
||||
done
|
||||
|
||||
echo "Error: port ${PORT} not served after ${waited}s; see $LOG_FILE" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
do_stop() {
|
||||
local pids=()
|
||||
mapfile -t pids < <(host_pids)
|
||||
if [[ ${#pids[@]} -eq 0 ]]; then
|
||||
echo "Not running"
|
||||
return 0
|
||||
fi
|
||||
|
||||
kill "${pids[@]}" 2>/dev/null || true
|
||||
|
||||
# Confirm the port is actually released rather than trusting the signal;
|
||||
# a half-dead instance would make the next start a no-op.
|
||||
local waited=0 remaining=()
|
||||
while [[ $waited -lt 10 ]]; do
|
||||
mapfile -t remaining < <(host_pids)
|
||||
if ! port_is_served && [[ ${#remaining[@]} -eq 0 ]]; then
|
||||
echo "Stopped Blender host (${pids[*]})"
|
||||
return 0
|
||||
fi
|
||||
command sleep 1
|
||||
waited=$((waited + 1))
|
||||
done
|
||||
|
||||
mapfile -t remaining < <(host_pids)
|
||||
[[ ${#remaining[@]} -gt 0 ]] && kill -9 "${remaining[@]}" 2>/dev/null || true
|
||||
command sleep 1
|
||||
if port_is_served; then
|
||||
echo "Error: port ${PORT} still served after SIGKILL" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Stopped Blender host (forced)"
|
||||
}
|
||||
|
||||
do_status() {
|
||||
if port_is_served; then
|
||||
echo "serving port=${PORT} pid=$(host_pid)"
|
||||
else
|
||||
echo "stopped port=${PORT}"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
validate_requirements
|
||||
case "${1:-start}" in
|
||||
start) do_start ;;
|
||||
stop) do_stop ;;
|
||||
status) do_status ;;
|
||||
restart) do_stop; command sleep 1; do_start ;;
|
||||
-h|--help|help) usage ;;
|
||||
*) echo "Unknown command: $1" >&2; exit 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
24
linux_configuration/scripts/blender-mcp-serve
Executable file
24
linux_configuration/scripts/blender-mcp-serve
Executable file
@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ============================================================================
|
||||
# MCP entrypoint: guarantee a Blender host exists, then serve the MCP protocol.
|
||||
#
|
||||
# Registered as the `blender` MCP server's command. The blender-mcp server
|
||||
# itself only *connects* to Blender's socket -- it cannot start Blender. If
|
||||
# nothing hosts port 9876 (e.g. after a reboot) the server comes up and every
|
||||
# tool call fails with a connection error, which reads like a broken MCP
|
||||
# rather than a missing host. Starting the host here makes that failure
|
||||
# impossible instead of relying on someone remembering to run a command.
|
||||
# ============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
readonly HOST_SCRIPT="/home/kuhy/.local/bin/blender-mcp-host"
|
||||
readonly SERVER_DIR="/home/kuhy/src/mcp-servers/servers/blender-mcp"
|
||||
readonly UV="/home/kuhy/.local/bin/uv"
|
||||
|
||||
# stdout is the MCP JSON-RPC channel -- any stray byte written there corrupts
|
||||
# the protocol, so all host-startup chatter goes to stderr.
|
||||
"$HOST_SCRIPT" start >&2
|
||||
|
||||
exec "$UV" run --directory "$SERVER_DIR" blender-mcp
|
||||
44
linux_configuration/scripts/fix-execstack-launch.sh
Executable file
44
linux_configuration/scripts/fix-execstack-launch.sh
Executable file
@ -0,0 +1,44 @@
|
||||
#!/bin/bash
|
||||
# Fix executable stack flags on Source Engine .so files before launching.
|
||||
# Usage: Set as Steam Launch Options:
|
||||
# ~/.local/bin/fix-execstack-launch.sh %command%
|
||||
#
|
||||
# Works for any Source Engine game that fails on modern kernels (6.x+)
|
||||
# due to shared libraries requiring executable stack (RWE flag).
|
||||
|
||||
if [[ $# -eq 0 ]]; then
|
||||
echo "Usage: $0 <command> [args...]"
|
||||
echo "Set as Steam launch option: ~/.local/bin/fix-execstack-launch.sh %command%"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Steam wraps %command% as: reaper SteamLaunch ... -- /path/to/game/hl2.sh -game ...
|
||||
# Find the game directory by looking for a path inside steamapps/common/ in the args.
|
||||
GAME_DIR=""
|
||||
for arg in "$@"; do
|
||||
if [[ "$arg" == */steamapps/common/* ]]; then
|
||||
resolved="$(readlink -f "$arg" 2>/dev/null)"
|
||||
if [[ -n "$resolved" ]]; then
|
||||
# Walk up to find the game root (directory directly under steamapps/common/)
|
||||
candidate="$resolved"
|
||||
while [[ "$candidate" == */steamapps/common/*/* ]]; do
|
||||
candidate="$(dirname "$candidate")"
|
||||
done
|
||||
GAME_DIR="$candidate"
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -z "$GAME_DIR" || ! -d "$GAME_DIR" ]]; then
|
||||
echo "[fix-execstack] Warning: Could not detect game directory, skipping patch."
|
||||
else
|
||||
# Fast path: directly patch known Source Engine offenders without scanning.
|
||||
# patchelf --clear-execstack is a no-op if the flag is already cleared.
|
||||
for sofile in "$GAME_DIR"/bin/engine.so "$GAME_DIR"/bin/video_bink.so; do
|
||||
[[ -f "$sofile" ]] && patchelf --clear-execstack "$sofile" 2>/dev/null
|
||||
done
|
||||
fi
|
||||
|
||||
# Launch the game
|
||||
exec "$@"
|
||||
39
linux_configuration/scripts/flutter-git-env-wrapper.sh
Executable file
39
linux_configuration/scripts/flutter-git-env-wrapper.sh
Executable file
@ -0,0 +1,39 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Strip the git-hook environment, then hand off to the real SDK binary.
|
||||
# Linked into ~/.local/bin by linux_configuration/fixes/fix_flutter_git_env.sh
|
||||
# -- see that script for why this exists. Invoked through a symlink named
|
||||
# after the tool it fronts (flutter, dart).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
tool="$(basename "$0")"
|
||||
real="/usr/bin/${tool}"
|
||||
|
||||
[[ -x $real ]] || {
|
||||
echo "flutter-git-env: $real is missing or not executable" >&2
|
||||
exit 127
|
||||
}
|
||||
|
||||
# Drop our own directory so a PATH lookup inside the real tool cannot find
|
||||
# this wrapper again. The Flutter entry point does exactly such a lookup.
|
||||
clean_path=""
|
||||
IFS=':' read -r -a path_parts <<<"$PATH"
|
||||
for part in "${path_parts[@]}"; do
|
||||
[[ $part == "$HOME/.local/bin" ]] && continue
|
||||
clean_path="${clean_path:+$clean_path:}$part"
|
||||
done
|
||||
|
||||
# GIT_DIR is the one that actually bites; the rest are exported by git in the
|
||||
# same breath and would mislead the SDK the same way.
|
||||
exec env \
|
||||
-u GIT_DIR \
|
||||
-u GIT_WORK_TREE \
|
||||
-u GIT_INDEX_FILE \
|
||||
-u GIT_COMMON_DIR \
|
||||
-u GIT_OBJECT_DIRECTORY \
|
||||
-u GIT_ALTERNATE_OBJECT_DIRECTORIES \
|
||||
-u GIT_NAMESPACE \
|
||||
-u GIT_PREFIX \
|
||||
PATH="$clean_path" \
|
||||
"$real" "$@"
|
||||
8
linux_configuration/scripts/krita-scaled.sh
Executable file
8
linux_configuration/scripts/krita-scaled.sh
Executable file
@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Scaled Krita launcher (shadows /usr/bin/krita on PATH).
|
||||
# QT_SCALE_FACTOR=2 -> whole GUI at 2x (crisp, image res & tablet unaffected)
|
||||
# XCURSOR_SIZE=32 -> ~2.5x on-screen cursor (visual only, no stroke change)
|
||||
# Tune the two numbers to taste. exec keeps the same PID/args.
|
||||
#
|
||||
exec env QT_SCALE_FACTOR=2 XCURSOR_SIZE=32 /usr/bin/krita "$@"
|
||||
38
linux_configuration/scripts/nvidia-pmon-logger.sh
Executable file
38
linux_configuration/scripts/nvidia-pmon-logger.sh
Executable file
@ -0,0 +1,38 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
LOG_DIR="$HOME/.local/share/gpu-log"
|
||||
ERR_LOG="$LOG_DIR/pmon-errors.log"
|
||||
mkdir -p "$LOG_DIR"
|
||||
|
||||
if ! command -v nvidia-smi >/dev/null 2>&1; then
|
||||
echo "nvidia-pmon-logger: nvidia-smi not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Wake every 60s to (a) check that the pmon child is still alive and (b) rotate
|
||||
# the log on day-rollover. This used to spin in a `read -r -t 20` loop, but
|
||||
# under systemd stdin is /dev/null, so `read` returned instantly on EOF. That
|
||||
# turned the loop into a hot fork-storm calling `date +%Y%m%d` thousands of
|
||||
# times per second (~22% of one core, ~122 CPU-hours/day).
|
||||
#
|
||||
# Use the bash printf %(...)T builtin (zero forks) for the date check, and a
|
||||
# real sleep (one fork/min, ~1440/day — negligible) for the wait.
|
||||
|
||||
while true; do
|
||||
printf -v day '%(%Y%m%d)T' -1
|
||||
out_file="$LOG_DIR/pmon-${day}.log"
|
||||
|
||||
nvidia-smi pmon -d 10 -o DT >>"$out_file" 2>>"$ERR_LOG" &
|
||||
pmon_pid=$!
|
||||
|
||||
while kill -0 "$pmon_pid" >/dev/null 2>&1; do
|
||||
printf -v today '%(%Y%m%d)T' -1
|
||||
if [[ $today != "$day" ]]; then
|
||||
kill "$pmon_pid" >/dev/null 2>&1 || true
|
||||
wait "$pmon_pid" 2>/dev/null || true
|
||||
break
|
||||
fi
|
||||
sleep 60
|
||||
done
|
||||
done
|
||||
8
linux_configuration/scripts/unreal-mcp-server
Executable file
8
linux_configuration/scripts/unreal-mcp-server
Executable file
@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
# Launch the unreal-mcp Python server over stdio.
|
||||
# ~/.local/bin/unreal-mcp-server is a symlink here, installed by
|
||||
# features/install_unreal_mcp.sh. Set UNREAL_MCP_PYTHON_DIR when the repo was
|
||||
# installed with --install-dir somewhere other than the default.
|
||||
set -euo pipefail
|
||||
python_dir="${UNREAL_MCP_PYTHON_DIR:-${HOME}/.local/share/unreal-mcp/unreal-mcp/Python}"
|
||||
exec uv --directory "${python_dir}" run unreal_mcp_server.py "${1:-}" </dev/null
|
||||
34
linux_configuration/scripts/usage-report-catchup.sh
Executable file
34
linux_configuration/scripts/usage-report-catchup.sh
Executable file
@ -0,0 +1,34 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
REPO="/home/kuhy/src/testsAndMisc"
|
||||
RUN_SCRIPT="$REPO/run.sh"
|
||||
OUT_DIR="$HOME/.local/share/usage-reports"
|
||||
ATOP_DIR="/var/log/atop"
|
||||
|
||||
mkdir -p "$OUT_DIR"
|
||||
|
||||
if [[ ! -x "$RUN_SCRIPT" ]]; then
|
||||
echo "usage-report-catchup: missing executable $RUN_SCRIPT" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
shopt -s nullglob
|
||||
TODAY="$(date +%Y%m%d)"
|
||||
for atop_file in "$ATOP_DIR"/atop_*; do
|
||||
date_part="${atop_file##*_}"
|
||||
if [[ ! "$date_part" =~ ^[0-9]{8}$ ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
out_file="$OUT_DIR/usage-report-${date_part}.md"
|
||||
tmp_file="$out_file.tmp"
|
||||
|
||||
if [[ "$date_part" == "$TODAY" || ! -s "$out_file" ]]; then
|
||||
if "$RUN_SCRIPT" --date "$date_part" >"$tmp_file"; then
|
||||
mv -f "$tmp_file" "$out_file"
|
||||
else
|
||||
rm -f "$tmp_file"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
Loading…
Reference in New Issue
Block a user