#!/usr/bin/env bash # ============================================================================= # test-tools.sh — Integration smoke-test for unraid-mcp MCP server tools # # Exercises every non-destructive action across all 10 tools using mcporter. # The server is launched ad-hoc via mcporter's --stdio flag so no persistent # process or registered server entry is required. # # Usage: # ./scripts/test-tools.sh [--timeout-ms N] [--parallel] [--verbose] # # Options: # --timeout-ms N Per-call timeout in milliseconds (default: 25000) # --parallel Run independent test groups in parallel (default: off) # --verbose Print raw mcporter output for each call # # Exit codes: # 0 — all tests passed or skipped # 1 — one or more tests failed # 2 — prerequisite check failed (mcporter, uv, server startup) # ============================================================================= set -Eeuo pipefail shopt -s inherit_errexit # --------------------------------------------------------------------------- # Constants # --------------------------------------------------------------------------- readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" readonly PROJECT_DIR="$(cd -- "${SCRIPT_DIR}/.." && pwd -P)" readonly SCRIPT_NAME="$(basename -- "${BASH_SOURCE[0]}")" readonly TS_START="$(date +%s%N)" # nanosecond epoch readonly LOG_FILE="${TMPDIR:-/tmp}/${SCRIPT_NAME%.sh}.$(date +%Y%m%d-%H%M%S).log" # Colours (disabled automatically when stdout is not a terminal) if [[ -t 1 ]]; then C_RESET='\033[0m' C_BOLD='\033[1m' C_GREEN='\033[0;32m' C_RED='\033[0;31m' C_YELLOW='\033[0;33m' C_CYAN='\033[0;36m' C_DIM='\033[2m' else C_RESET='' C_BOLD='' C_GREEN='' C_RED='' C_YELLOW='' C_CYAN='' C_DIM='' fi # --------------------------------------------------------------------------- # Defaults (overridable via flags) # --------------------------------------------------------------------------- CALL_TIMEOUT_MS=25000 USE_PARALLEL=false VERBOSE=false # --------------------------------------------------------------------------- # Counters (updated by run_test / skip_test) # --------------------------------------------------------------------------- PASS_COUNT=0 FAIL_COUNT=0 SKIP_COUNT=0 declare -a FAIL_NAMES=() # --------------------------------------------------------------------------- # Argument parsing # --------------------------------------------------------------------------- parse_args() { while [[ $# -gt 0 ]]; do case "$1" in --timeout-ms) CALL_TIMEOUT_MS="${2:?--timeout-ms requires a value}" shift 2 ;; --parallel) USE_PARALLEL=true shift ;; --verbose) VERBOSE=true shift ;; -h|--help) printf 'Usage: %s [--timeout-ms N] [--parallel] [--verbose]\n' "${SCRIPT_NAME}" exit 0 ;; *) printf '[ERROR] Unknown argument: %s\n' "$1" >&2 exit 2 ;; esac done } # --------------------------------------------------------------------------- # Logging helpers # --------------------------------------------------------------------------- log_info() { printf "${C_CYAN}[INFO]${C_RESET} %s\n" "$*" | tee -a "${LOG_FILE}"; } log_warn() { printf "${C_YELLOW}[WARN]${C_RESET} %s\n" "$*" | tee -a "${LOG_FILE}"; } log_error() { printf "${C_RED}[ERROR]${C_RESET} %s\n" "$*" | tee -a "${LOG_FILE}" >&2; } elapsed_ms() { local now now="$(date +%s%N)" printf '%d' "$(( (now - TS_START) / 1000000 ))" } # --------------------------------------------------------------------------- # Cleanup trap # --------------------------------------------------------------------------- cleanup() { local rc=$? if [[ $rc -ne 0 ]]; then log_warn "Script exited with rc=${rc}. Log: ${LOG_FILE}" fi } trap cleanup EXIT # --------------------------------------------------------------------------- # Prerequisite checks # --------------------------------------------------------------------------- check_prerequisites() { local missing=false if ! command -v mcporter &>/dev/null; then log_error "mcporter not found in PATH. Install it and re-run." missing=true fi if ! command -v uv &>/dev/null; then log_error "uv not found in PATH. Install it and re-run." missing=true fi if ! command -v python3 &>/dev/null; then log_error "python3 not found in PATH." missing=true fi if [[ ! -f "${PROJECT_DIR}/pyproject.toml" ]]; then log_error "pyproject.toml not found at ${PROJECT_DIR}. Wrong directory?" missing=true fi if [[ "${missing}" == true ]]; then return 2 fi } # --------------------------------------------------------------------------- # Server startup smoke-test # Launches the stdio server and calls unraid_health action=check. # Returns 0 if the server responds (even with an API error — that still # means the Python process started cleanly), non-zero on import failure. # --------------------------------------------------------------------------- smoke_test_server() { log_info "Smoke-testing server startup..." local output output="$( mcporter call \ --stdio "uv run unraid-mcp-server" \ --cwd "${PROJECT_DIR}" \ --name "unraid-smoke" \ --tool unraid_health \ --args '{"action":"check"}' \ --timeout 30000 \ --output json \ 2>&1 )" || true # If mcporter returns the offline error the server failed to import/start if printf '%s' "${output}" | grep -q '"kind": "offline"'; then log_error "Server failed to start. Output:" printf '%s\n' "${output}" >&2 log_error "Common causes:" log_error " • Missing module: check 'uv run unraid-mcp-server' locally" log_error " • server.py has an import for a file that doesn't exist yet" log_error " • Environment variable UNRAID_API_URL or UNRAID_API_KEY missing" return 2 fi log_info "Server started successfully (health response received)." return 0 } # --------------------------------------------------------------------------- # mcporter call wrapper # Usage: mcporter_call # Writes the mcporter JSON output to stdout. # Returns the mcporter exit code. # --------------------------------------------------------------------------- mcporter_call() { local tool_name="${1:?tool_name required}" local args_json="${2:?args_json required}" mcporter call \ --stdio "uv run unraid-mcp-server" \ --cwd "${PROJECT_DIR}" \ --name "unraid" \ --tool "${tool_name}" \ --args "${args_json}" \ --timeout "${CALL_TIMEOUT_MS}" \ --output json \ 2>&1 } # --------------------------------------------------------------------------- # Test runner # Usage: run_test