Add Exa connector: mcp_remote, neural web search, optional API key (free tier), verify.py with MCP initialize probe
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
@@ -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: <reason>"}
|
||||
|
||||
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()
|
||||
Reference in New Issue
Block a user