feat: add API key bearer token authentication

- ApiKeyVerifier(TokenVerifier) — validates Authorization: Bearer <key>
  against UNRAID_MCP_API_KEY; guards against empty-key bypass
- _build_auth() replaces module-level _build_google_auth() call:
  returns MultiAuth(server=google, verifiers=[api_key]) when both set,
  GoogleProvider alone, ApiKeyVerifier alone, or None
- settings.py: add UNRAID_MCP_API_KEY + is_api_key_auth_configured()
  + api_key_auth_enabled in get_config_summary()
- run_server(): improved auth status logging for all three states
- tests/test_api_key_auth.py: 9 tests covering verifier + _build_auth
- .env.example: add UNRAID_MCP_API_KEY section
- docs/GOOGLE_OAUTH.md: add API Key section
- README.md / CLAUDE.md: rename section, document both auth methods
- Fix pre-existing: test_health.py patched cache_middleware/error_middleware
  now match renamed _cache_middleware/_error_middleware in server.py
This commit is contained in:
Jacob Magar
2026-03-16 11:11:38 -04:00
parent 6f7a58a0f9
commit cc24f1ec62
16 changed files with 406 additions and 69 deletions

View File

@@ -134,6 +134,11 @@ check_prerequisites() {
missing=true
fi
if ! command -v jq &>/dev/null; then
log_error "jq not found in PATH. Install it and re-run."
missing=true
fi
if [[ ! -f "${PROJECT_DIR}/pyproject.toml" ]]; then
log_error "pyproject.toml not found at ${PROJECT_DIR}. Wrong directory?"
missing=true
@@ -181,10 +186,12 @@ smoke_test_server() {
import sys, json
try:
d = json.load(sys.stdin)
if 'status' in d or 'success' in d or 'error' in d:
if 'error' in d:
print('error: tool returned error key — ' + str(d.get('error', '')))
elif 'status' in d or 'success' in d:
print('ok')
else:
print('missing: no status/success/error key in response')
print('missing: no status/success key in response')
except Exception as e:
print('parse_error: ' + str(e))
" 2>/dev/null
@@ -253,6 +260,31 @@ run_test() {
return 1
fi
# Always validate JSON is parseable and not an error payload
local json_check
json_check="$(
printf '%s' "${output}" | python3 -c "
import sys, json
try:
d = json.load(sys.stdin)
if isinstance(d, dict) and ('error' in d or d.get('kind') == 'error'):
print('error: ' + str(d.get('error', d.get('message', 'unknown error'))))
else:
print('ok')
except Exception as e:
print('invalid_json: ' + str(e))
" 2>/dev/null
)" || json_check="parse_error"
if [[ "${json_check}" != "ok" ]]; then
printf "${C_RED}[FAIL]${C_RESET} %-55s ${C_DIM}%dms${C_RESET}\n" \
"${label}" "${elapsed_ms}" | tee -a "${LOG_FILE}"
printf ' response validation failed: %s\n' "${json_check}" | tee -a "${LOG_FILE}"
FAIL_COUNT=$(( FAIL_COUNT + 1 ))
FAIL_NAMES+=("${label}")
return 1
fi
# Validate optional key presence
if [[ -n "${expected_key}" ]]; then
local key_check