#!/usr/bin/env bash # ============================================================================= # test-tools.sh — Integration smoke-test for unraid-mcp MCP server tools # # Exercises broad non-destructive smoke coverage of the consolidated `unraid` tool # (action + subaction pattern). The server is launched ad-hoc via mcporter's # --stdio flag so no persistent process or registered server entry is required. # # Usage: # ./tests/mcporter/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 -uo pipefail # --------------------------------------------------------------------------- # 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 action=health subaction=check. # Returns 0 if the server responds, 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 \ --args '{"action":"health","subaction":"check"}' \ --timeout 30000 \ --output json \ 2>&1 )" || true 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 local key_check key_check="$( printf '%s' "${output}" | python3 -c " import sys, json try: d = json.load(sys.stdin) if 'status' in d or 'success' in d or 'error' in d: print('ok') else: print('missing: no status/success/error key in response') except Exception as e: print('parse_error: ' + str(e)) " 2>/dev/null )" || key_check="parse_error" if [[ "${key_check}" != "ok" ]]; then log_error "Smoke test: unexpected response shape — ${key_check}" printf '%s\n' "${output}" >&2 return 2 fi log_info "Server started successfully (health response received)." return 0 } # --------------------------------------------------------------------------- # mcporter call wrapper # Usage: mcporter_call # All calls go to the single `unraid` tool. # --------------------------------------------------------------------------- mcporter_call() { local args_json="${1:?args_json required}" mcporter call \ --stdio "uv run unraid-mcp-server" \ --cwd "${PROJECT_DIR}" \ --name "unraid" \ --tool unraid \ --args "${args_json}" \ --timeout "${CALL_TIMEOUT_MS}" \ --output json \ 2>&1 } # --------------------------------------------------------------------------- # Test runner # Usage: run_test