unraid-mcp/Justfile

172 lines
6.7 KiB
Makefile
Raw Permalink Normal View History

# Unraid MCP Server — Justfile
# Run `just` to list available recipes
default:
@just --list
# ── Development ───────────────────────────────────────────────────────────────
# Start development server (hot-reload via watchfiles)
dev:
uv run python -m unraid_mcp
# Run tests
test:
uv run pytest tests/ -v
# Run linter (ruff)
lint:
uv run ruff check .
# Format code (ruff)
fmt:
uv run ruff format .
# Type-check (pyright / mypy)
typecheck:
uv run pyright unraid_mcp/ || uv run mypy unraid_mcp/
# Validate skills SKILL.md files exist and are non-empty
validate-skills:
@echo "=== Validating skills ==="
@for f in skills/*/SKILL.md; do \
if [ -f "$$f" ]; then \
echo "$$f"; \
else \
echo " ✗ SKILL.md not found in skills/"; \
exit 1; \
fi; \
done
@echo "Skills validation passed"
# Build Docker image
build:
docker build -t unraid-mcp .
# ── Docker Compose ────────────────────────────────────────────────────────────
# Start containers
up:
docker compose up -d
# Stop containers
down:
docker compose down
# Restart containers
restart:
docker compose restart
# Tail container logs
logs:
docker compose logs -f
# Check /health endpoint
health:
@PORT=$$(grep -E '^UNRAID_MCP_PORT=' .env 2>/dev/null | cut -d= -f2 || echo 6970); \
curl -sf "http://localhost:$$PORT/health" | python3 -m json.tool || echo "Health check failed"
# ── Live Testing ──────────────────────────────────────────────────────────────
# Run live integration tests against a running server
test-live:
uv run pytest tests/ -v -m live
# Run HTTP end-to-end smoke-test against the local server (auto-reads token from ~/.unraid-mcp/.env)
test-http:
bash tests/mcporter/test-http.sh
# Run HTTP e2e test with auth disabled (for gateway-protected deployments)
test-http-no-auth:
bash tests/mcporter/test-http.sh --skip-auth
# Run HTTP e2e test against a remote URL
# Usage: just test-http-remote https://unraid.tootie.tv/mcp
test-http-remote url:
bash tests/mcporter/test-http.sh --url {{url}} --skip-auth
# ── Setup ─────────────────────────────────────────────────────────────────────
# Create .env from .env.example if missing
setup:
@if [ ! -f .env ]; then \
cp .env.example .env && chmod 600 .env; \
echo "Created .env from .env.example — fill in your credentials"; \
else \
echo ".env already exists"; \
fi
# Generate a secure bearer token for UNRAID_MCP_TOKEN
gen-token:
@python3 -c "import secrets; print(secrets.token_urlsafe(32))"
# ── Quality Gates ─────────────────────────────────────────────────────────────
# Run docker security checks
check-contract:
bash bin/check-docker-security.sh
bash bin/check-no-baked-env.sh
bash bin/ensure-ignore-files.sh --check
# ── CLI Generation ────────────────────────────────────────────────────────────
# Generate a standalone CLI for this server (requires running server)
generate-cli:
#!/usr/bin/env bash
set -euo pipefail
PORT="${UNRAID_MCP_PORT:-6970}"
echo "⚠ Server must be running on port ${PORT} (run 'just dev' first)"
echo "⚠ Generated CLI embeds your OAuth token — do not commit or share"
mkdir -p dist dist/.cache
current_hash=$(timeout 10 curl -sf \
-H "Authorization: Bearer $MCP_TOKEN" \
-H "Accept: application/json, text/event-stream" \
"http://localhost:${PORT}/mcp/tools/list" 2>/dev/null | sha256sum | cut -d' ' -f1 || echo "nohash")
cache_file="dist/.cache/unraid-mcp-cli.schema_hash"
if [[ -f "$cache_file" ]] && [[ "$(cat "$cache_file")" == "$current_hash" ]] && [[ -f "dist/unraid-mcp-cli" ]]; then
echo "SKIP: unraid-mcp tool schema unchanged — use existing dist/unraid-mcp-cli"
exit 0
fi
timeout 30 mcporter generate-cli \
--command "http://localhost:${PORT}/mcp" \
--header "Authorization: Bearer $MCP_TOKEN" \
--name unraid-mcp-cli \
--output dist/unraid-mcp-cli
printf '%s' "$current_hash" > "$cache_file"
echo "✓ Generated dist/unraid-mcp-cli (requires bun at runtime)"
# ── Cleanup ───────────────────────────────────────────────────────────────────
# Remove build artifacts, caches, and compiled files
clean:
rm -rf dist/ build/ .pytest_cache/ .ruff_cache/ .mypy_cache/ htmlcov/ .coverage coverage.xml
find . -type d -name __pycache__ -not -path './.venv/*' -exec rm -rf {} + 2>/dev/null || true
find . -name '*.pyc' -not -path './.venv/*' -delete 2>/dev/null || true
@echo "Cleaned build artifacts"
# Publish: bump version, tag, push (triggers PyPI + Docker publish)
publish bump="patch":
#!/usr/bin/env bash
set -euo pipefail
[ "$(git branch --show-current)" = "main" ] || { echo "Switch to main first"; exit 1; }
[ -z "$(git status --porcelain)" ] || { echo "Commit or stash changes first"; exit 1; }
git pull origin main
CURRENT=$(grep -m1 "^version" pyproject.toml | sed "s/.*\"\(.*\)\".*/\1/")
IFS="." read -r major minor patch <<< "$CURRENT"
case "{{bump}}" in
major) major=$((major+1)); minor=0; patch=0 ;;
minor) minor=$((minor+1)); patch=0 ;;
patch) patch=$((patch+1)) ;;
*) echo "Usage: just publish [major|minor|patch]"; exit 1 ;;
esac
NEW="${major}.${minor}.${patch}"
echo "Version: ${CURRENT}${NEW}"
sed -i "s/^version = \"${CURRENT}\"/version = \"${NEW}\"/" pyproject.toml
for f in .claude-plugin/plugin.json .codex-plugin/plugin.json gemini-extension.json; do
[ -f "$f" ] && python3 -c "import json; d=json.load(open(\"$f\")); d[\"version\"]=\"${NEW}\"; json.dump(d,open(\"$f\",\"w\"),indent=2); open(\"$f\",\"a\").write(\"
\")"
done
git add -A && git commit -m "release: v${NEW}" && git tag "v${NEW}" && git push origin main --tags
echo "Tagged v${NEW} — publish workflow will run automatically"