#!/usr/bin/env python3 """Verify an Exa API key (or free-tier reachability). Reads exaApiKey from the environment (optional — Exa works in free tier without one) and prints a single JSON object: {"ok": true, "message": "Exa API key is valid"} {"ok": false, "message": "Exa API key is invalid or unauthorized"} With a key, the probe is a minimal `POST https://api.exa.ai/search` carrying the key in the `x-api-key` header — the only call that actually exercises the credential. Exa's MCP endpoint answers `initialize` with HTTP 200 no matter what key is on the URL, so it cannot tell a good key from a bad one; without a key we therefore only report reachability, never validity. Two headers are mandatory on every request: * `User-Agent` — mcp.exa.ai and api.exa.ai sit behind Cloudflare, which answers a request carrying urllib's default agent with HTTP 403 / "error code: 1010" (browser-signature ban) before Exa ever sees the key. * `Accept: application/json, text/event-stream` — the MCP streamable-http endpoint rejects anything else with HTTP 406. A non-2xx status is only reported as an invalid key when Exa itself says so in a JSON error body; an opaque 401/403 (Cloudflare, a proxy) is reported as blocked, because mapping a bare status code to "bad key" is what made this script reject valid keys. The API key is never printed. stdlib only (urllib). """ import json import os import sys import urllib.error import urllib.request SEARCH_URL = "https://api.exa.ai/search" MCP_URL = "https://mcp.exa.ai/mcp" USER_AGENT = "skald-verify/1.0" TIMEOUT = 12 def _result(ok, message, details=None): payload = {"ok": ok, "message": message} if details: payload["details"] = details print(json.dumps(payload)) sys.exit(0 if ok else 1) def _post(url, headers, body): """POST and return (status, body_text). Raises only on transport errors.""" req = urllib.request.Request( url, data=json.dumps(body).encode("utf-8"), headers={"Content-Type": "application/json", "User-Agent": USER_AGENT, **headers}, method="POST", ) try: with urllib.request.urlopen(req, timeout=TIMEOUT) as resp: return resp.status, resp.read().decode("utf-8", "replace") except urllib.error.HTTPError as e: return e.code, e.read().decode("utf-8", "replace") def _exa_error(text): """Exa's own error message from a JSON error body, or None if not ours.""" try: data = json.loads(text) except (ValueError, TypeError): return None if isinstance(data, dict) and "error" in data: return str(data["error"]) return None def check_key(api_key): """Probe the REST API with the key. Only this call validates a credential.""" try: status, text = _post( SEARCH_URL, {"Accept": "application/json", "x-api-key": api_key}, {"query": "skald connector verification", "numResults": 1}, ) except Exception as e: _result(False, f"Exa API is unreachable: {e}") if 200 <= status < 300: _result(True, "Exa API key is valid") detail = _exa_error(text) if status in (401, 403): if detail: _result(False, f"Exa API key is invalid or unauthorized: {detail}") # Opaque rejection — Cloudflare or a proxy, not Exa judging the key. _result(False, f"Request to Exa was blocked before reaching the API " f"(HTTP {status}). The key was not tested.") if status == 402: _result(False, "Exa account is out of credits " f"({detail or 'HTTP 402'})") if status == 429: # A rate limit proves the key was accepted, just throttled right now. _result(True, "Exa API key is valid (rate limit currently reached)") _result(False, f"Exa returned HTTP {status}" + (f": {detail}" if detail else "")) def check_free_tier(): """No key configured: report whether the MCP endpoint answers at all.""" try: status, text = _post( MCP_URL, {"Accept": "application/json, text/event-stream"}, {"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2025-03-26", "capabilities": {}, "clientInfo": {"name": "skald-verify", "version": "1.0.0"}}}, ) except Exception as e: _result(False, f"Exa MCP endpoint is unreachable: {e}") if 200 <= status < 300: _result(True, "Exa MCP endpoint is reachable (free tier, no API key)") detail = _exa_error(text) _result(False, f"Exa MCP endpoint returned HTTP {status}" + (f": {detail}" if detail else "")) def main(): api_key = (os.environ.get("exaApiKey") or os.environ.get("EXA_API_KEY") or "").strip() if api_key: check_key(api_key) else: check_free_tier() if __name__ == "__main__": main()