From 341e4382c8fc6a2a2dd69d6ad3d058c79f0fb456 Mon Sep 17 00:00:00 2001 From: xavix-yo Date: Sun, 19 Jul 2026 14:13:28 +0100 Subject: [PATCH] Add Exa connector: mcp_remote, neural web search, optional API key (free tier), verify.py with MCP initialize probe --- connectors/connectors.json | 44 +++++++++++++++++++++ connectors/exa/connector.json | 46 ++++++++++++++++++++++ connectors/exa/icon_lg.png | Bin 0 -> 3730 bytes connectors/exa/icon_sm.png | Bin 0 -> 1728 bytes connectors/exa/verify.py | 70 ++++++++++++++++++++++++++++++++++ 5 files changed, 160 insertions(+) create mode 100644 connectors/exa/connector.json create mode 100644 connectors/exa/icon_lg.png create mode 100644 connectors/exa/icon_sm.png create mode 100644 connectors/exa/verify.py 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 0000000000000000000000000000000000000000..dcd07a4c6cbad5119592686c39fdc4ccb59af45d GIT binary patch literal 3730 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 literal 0 HcmV?d00001 diff --git a/connectors/exa/icon_sm.png b/connectors/exa/icon_sm.png new file mode 100644 index 0000000000000000000000000000000000000000..8b6ab9a17acc90b8df8871d1828cb8744904d616 GIT binary patch literal 1728 zcmV;x20!_UP)K|xuK?Ng<3&t(PB^pJe_=hXFAQ-Ha-B2iXseph8f{Gg4 zq7q_|#C^df#sC^c6Tpz*7LW=-pry+!_nLEWN0G(OyqSV?l9%Z+@7?p=^L^)>d!DlK zKP-7{nmjj6o|`7mO_S#qPWOw}g0Y~w*B>OKXa!2;7=S8(5C_o`IRYR%uQW6@!7rN? zVY#Eb60d?}5~?dL#~cmTbRCH<0SIkEA*g>!^WrkaVHJK6!Qh^q4X5ls&yml9Ff?t0 z+B+=M#~Vz-ubmZulz;I!ltdv~2ub7r5tlIkjU*U=aM<52j<&SHIsKq^sKq8h>s)F; zfid3SSOO{!hXs=jp-xJrE4q{=6HroSS@eeH^>>upoT36)P-m#Bw3t|q0qm)Xq6lnm zf>l2#YKBTUg4u?RiceN5qQ$VQ1)iR2=-bDV?C6?0fdPo^Zo@4)ZGqyrqZQX*X^FK# zIE_UmiYg4-cfc**8T{Q;?4)^17AWj z74n~>8#O;~@_HI{u4VFl1`|(@Hh}?zG&JvqhwnEGA7b6l9UU;dKE$q_-Z`PzflQnY z6~K%|ns>fXL?Uop70jzM?%6@KlyL-Pf!21IINWeye@h|>>wkg=8nvqz5~2NpkklJ4 zgykP9W-nG$l=^2atQXNjGQjQ!2H?`Cf(26yJu9KO5SD$cnDx9S6!ARY)8zM1A*|b= zsGY8fM!jestudV4FLT~!1whP&8#wkD4?-O5Q3f-fSG>DIIcH_Zo%2GA5bWFq_fFNc zbwG0q+YRo#jheL^y$&uo z6B=qdE3L}LgDe3^+Dyh^*bvLm>nts;zM9@!uW4xktGsCEyr6mWBjo^A9S#ep8Yf`V z=ykdUfKVoN$rcL@9(urV)~UY2uK5va8#Unwy#JZvvF8*$%6&wHa@9Sp1Fmy$8p*s8 z^j-+3o!1&IU{m%wEMEel^)RFuKuWs0(>uiUp?Yq{$tzw@aiaL@q=Wuhg}xJZb* zFL@>ZOGc3jE%o8Ba%gUa^UttMAL~V%$!RMc3$z#i<$!A!_W5JyJ>75+3TN_V;k{Jh zDI*L6&e%VNmVKa@|Ej0ZpSy4r`tn=F_?em~CL1r_E;gat0LYp`jlL&A|I_x}&aKOr7HCy(SQgs)gnt+3L;SVgl6>*erNlgY?(FDIEr0< zk!9jt)}>K7^NwuLZ2F!3_Dn%6tAr-ID2}|aMDgNVin0>iB`2#=={Bve)T4(Pt{-Gw zWSX|aeGQse%&%_kAp{VK!dG7_rp(inm*N_=xswdNkN4D)lq%7Z@yP5+hCU~F(SPxc zV(MIt4rd>Qz^#PXrp+*-L8GPHY=QB^3|CzI@Agc{XB+{c!nmJ@o90y>39rsDuHlnNKJ}Vn>D!s>A>;FaP(%h%)kncOeJ!iLQ^7}zrIt$u zSeCr!Ysk3AH7E8;(zg$}A1zAQ-Ed0sf7qwZA+ZMN_s*Xr+yxt|o zl%}K&5YD%D_=V(O9la=2qo_tBWNk%$)P+h$zPQBpRvK4;) zy_;Wf;GM2M%2HjGUIe-Xklh&gfAESaWj3DgFZopVL#XaBO`e-3&rOr(rVq6P+1g)7 WBFEgBiBI+b0000"} + +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()