diff --git a/connectors/connectors.json b/connectors/connectors.json index f5f3168..2a5d828 100644 --- a/connectors/connectors.json +++ b/connectors/connectors.json @@ -174,6 +174,50 @@ } ] }, + { + "id": "exa", + "name": "Exa", + "type": "mcp_remote", + "scope": "global", + "icon_small": "exa/icon_sm.png", + "icon_large": "exa/icon_lg.png", + "user_description": "AI-powered web search, content extraction, and research via Exa's neural search engine. Works without an API key (free rate-limited tier).", + "tags": [ + "search", + "mcp", + "remote", + "ai" + ], + "auth": { + "type": "api_key" + }, + "folder": "exa", + "version": 1, + "version_string": "1.0.0", + "version_release_date": "2026-07-19", + "files": [ + { + "path": "connector.json", + "sha256": "65d979906e37daee32c0f9e53f29b62a2003f44c8c3ae7899c0bdab78e4c78c0", + "size": 1409 + }, + { + "path": "verify.py", + "sha256": "8e77da3ca3eca8f733296da134aaa964e5db8d01d4bfb89aa5832296e9dc45a6", + "size": 2125 + }, + { + "path": "icon_sm.png", + "sha256": "dd0a17d75ce36187f34c1fbfa459aee1665d94b2b8e1e300c2559631ffdcd1bd", + "size": 1728 + }, + { + "path": "icon_lg.png", + "sha256": "a1b698035af2cb146dccd57f992155359d55a48bc85802e2a02e8148dcec1558", + "size": 3730 + } + ] + }, { "id": "firecrawl", "name": "Firecrawl", diff --git a/connectors/exa/connector.json b/connectors/exa/connector.json new file mode 100644 index 0000000..ae21c0d --- /dev/null +++ b/connectors/exa/connector.json @@ -0,0 +1,46 @@ +{ + "id": "exa", + "name": "Exa", + "version": 1, + "version_string": "1.0.0", + "version_release_date": "2026-07-19", + "type": "mcp_remote", + "scope": "global", + "tags": [ + "search", + "mcp", + "remote", + "ai" + ], + "docs": [ + { + "lang": "en", + "description": "AI-powered web search, content extraction, and embeddings via Exa's neural search engine. Perfect for deep research, finding specific information, and analyzing page content. Works without an API key (free rate-limited tier) — add your own key for higher limits.", + "llm_short_description": "Search the web and extract page content using Exa's neural search engine. Tools: web_search_exa, web_fetch_exa." + } + ], + "auth": { + "type": "api_key" + }, + "env": [ + { + "name": "exaApiKey", + "label": "Exa API key (optional)", + "description": "Get one at https://dashboard.exa.ai/api-keys. Without it, Exa works with free rate limits. Add your key for unlimited production use.", + "required": false, + "secret": true, + "example": "exa-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + } + ], + "mcp_config": { + "url": "https://mcp.exa.ai/mcp?exaApiKey={SECRET:exaApiKey}", + "transport": "streamable-http" + }, + "verify": { + "command": "python3 verify.py", + "timeout_secs": 15 + }, + "homepage": "https://exa.ai", + "icon_small": "icon_sm.png", + "icon_large": "icon_lg.png" +} diff --git a/connectors/exa/icon_lg.png b/connectors/exa/icon_lg.png new file mode 100644 index 0000000..dcd07a4 Binary files /dev/null and b/connectors/exa/icon_lg.png differ diff --git a/connectors/exa/icon_sm.png b/connectors/exa/icon_sm.png new file mode 100644 index 0000000..8b6ab9a Binary files /dev/null and b/connectors/exa/icon_sm.png differ diff --git a/connectors/exa/verify.py b/connectors/exa/verify.py new file mode 100644 index 0000000..a4b20ff --- /dev/null +++ b/connectors/exa/verify.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +"""Verify connectivity to Exa's hosted MCP server. + +Reads exaApiKey from the environment (optional — Exa works in free tier without +one), sends a lightweight JSON-RPC initialize to https://mcp.exa.ai/mcp, and +prints a single JSON object: + + {"ok": true, "message": "Exa MCP endpoint is reachable"} + {"ok": false, "message": "Exa MCP endpoint is unreachable: "} + +The API key is never printed. HTTP-level verification only — no search queries. +stdlib only (urllib). +""" +import json +import os +import sys +import urllib.error +import urllib.request + + +def _result(ok, message): + print(json.dumps({"ok": ok, "message": message})) + sys.exit(0 if ok else 1) + + +def main(): + api_key = os.environ.get("exaApiKey", "").strip() + # Also accept UPPER_SNAKE form + if not api_key: + api_key = os.environ.get("EXA_API_KEY", "").strip() + + url = "https://mcp.exa.ai/mcp" + if api_key: + url += f"?exaApiKey={api_key}" + + body = json.dumps({ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": {"name": "skald-verify", "version": "1.0.0"}, + }, + }).encode("utf-8") + + req = urllib.request.Request( + url, + data=body, + headers={"Content-Type": "application/json"}, + method="POST", + ) + + try: + with urllib.request.urlopen(req, timeout=15) as resp: + if 200 <= resp.status < 300: + label = "with API key" if api_key else "free tier" + _result(True, f"Exa MCP endpoint is reachable ({label})") + _result(False, f"Exa returned HTTP {resp.status}") + except urllib.error.HTTPError as e: + if e.code in (401, 403): + _result(False, "Exa API key is invalid or unauthorized " + f"(HTTP {e.code})") + _result(False, f"Exa returned HTTP {e.code}: {e.reason}") + except Exception as e: + _result(False, f"Exa MCP endpoint is unreachable: {e}") + + +if __name__ == "__main__": + main()