Activating Exa always failed with "Exa API key is invalid or unauthorized (HTTP 403)" — the key was never actually tested. Two bugs stacked: 1. mcp.exa.ai is behind Cloudflare, which bans urllib's default Python-urllib/3.x agent with 403 / "error code: 1010" before Exa sees the request. verify.py sent no User-Agent and mapped any 403 to "API key is invalid". Reproduced on the server with no key set at all — same "invalid key" message. 2. The probed endpoint cannot validate a key anyway: JSON-RPC initialize against the MCP endpoint returns 200 no matter what ?exaApiKey= carries (checked with a real key, a bogus key, and no key). Fixing only the headers would have flipped the bug to accepting every key, including garbage. With a key, the probe is now a minimal POST to api.exa.ai/search with the key in the x-api-key header — the only call that exercises the credential (200 valid, 401/403 + Exa JSON error invalid, 402 out of credits, 429 valid but throttled). With no key it probes MCP initialize and reports reachability only, never validity. Both requests send a User-Agent; the MCP one also sends Accept: application/json, text/event-stream (else HTTP 406). An opaque 401/403 with no Exa JSON error is now reported as "blocked before reaching the API — the key was not tested", instead of blaming the credential. Also re-aligns manifest/fragment versions to 5 / 1.0.4 (were 2/1.0.1 vs 4/1.0.3; skald reads installed_version from the manifest, so the update badge would never have appeared) and raises verify.timeout_secs 15 -> 20.
14 KiB
14 KiB
Changelog
All notable changes to the Skald Connectors Marketplace are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]
2026-08-20
Fixed
- exa:
verify.pyrejected every API key, valid or not (v5 / 1.0.4) — activating Exa always failed withExa API key is invalid or unauthorized (HTTP 403). The key was never actually tested; two independent bugs stacked up.- Cloudflare blocked the probe before Exa saw it.
mcp.exa.aisits behind Cloudflare, which bans urllib's defaultPython-urllib/3.xagent with HTTP 403 /error code: 1010(browser-signature ban). The script sent noUser-Agent, and mapped any 403 straight to "API key is invalid" — so the message named the key for a failure the key had nothing to do with. Reproduced on the server with no key set at all:{"ok": false, "message": "Exa API key is invalid or unauthorized (HTTP 403)"}. - The endpoint it probed cannot validate a key anyway. The probe was a JSON-RPC
initializeagainst the MCP endpoint, which answers HTTP 200 regardless of what?exaApiKey=carries — verified against a real key, a syntactically valid bogus key, and no key at all. Fixing only the headers would have flipped the bug to the opposite failure: every key accepted, including garbage. - Fix — when a key is configured the probe is now a minimal
POST https://api.exa.ai/search(numResults: 1) with the key in thex-api-keyheader, the only call that exercises the credential: 200 → valid, 401/403 with Exa's own JSON error → invalid, 402 → out of credits, 429 → valid but throttled. With no key configured (Exa's free tier) it probes MCPinitializeand reports reachability only, never validity. Both requests now send aUser-Agent, and the MCP one also sendsAccept: application/json, text/event-stream(without it the endpoint returns HTTP 406,Client must accept both application/json and text/event-stream). - A non-2xx status is reported as an invalid key only when Exa itself says so in a JSON error body; an opaque 401/403 (Cloudflare, a proxy) is now reported as "blocked before reaching the API — the key was not tested". Mapping a bare status code to "bad key" is precisely what made this script reject valid keys.
- Re-aligned manifest↔fragment versions to
5/1.0.4(they were 2/1.0.1 vs 4/1.0.3; skald readsinstalled_versionfrom the manifest, so the update badge would never have appeared).verify.timeout_secs15 → 20, for margin over the script's own 12s per-request timeout. - Tested end-to-end: valid key →
{"ok": true, "message": "Exa API key is valid"}; bogus key →{"ok": false, "message": "Exa API key is invalid or unauthorized: Invalid API key"}; no key →{"ok": true, "message": "Exa MCP endpoint is reachable (free tier, no API key)"}✅. Runtime path also confirmed unaffected:initialize+notifications/initialized+ a realweb_search_exacall againsthttps://mcp.exa.ai/mcp?exaApiKey=…returned results ✅ - ⚠️ Same latent pattern elsewhere:
context7,tavily, andserpapi-flightsalso probe over urllib with noUser-Agentand map bare 401/403 to a credential verdict. They pass today because their hosts do not run Cloudflare's browser-signature check — not because the scripts are correct.
- Cloudflare blocked the probe before Exa saw it.
2026-08-19
Added
- New connector: Playwright (mcp_local, scope global, v1 / 1.0.0) — full browser automation via the official
@playwright/mcp@0.0.79(Microsoft), wrapped like http-fetch/firecrawl:package.jsonpins the package andindex.jsrewrites argv (--headless --isolated --no-sandbox, plus aprocess.argv.slice(2)passthrough) and imports the package'scli.js, which self-executes at import time.cli.jsis not in the packageexportsmap, so the wrapper resolves it from the exportedpackage.jsonpath.auth: none,requires: ["NODE"]. Runs headless with an isolated in-memory profile: no cookies/login state persisted between sessions (chosen over the default persistent profile — a shared household browser must not accumulate per-user sessions, and a persistent profile also allows only one browser instance at a time).- Browser download: the upstream
playwrightpackage does NOT download browsers atnpm install(verified on 1.63.0-alpha-2026-08-05), and@playwright/mcp0.0.79 no longer ships the oldbrowser_installtool (the README section is now empty). So the connector'spackage.jsoncarries apostinstallhook —node node_modules/@playwright/mcp/cli.js install-browser chromium— which downloads Chromium + headless shell + ffmpeg only (~350 MB; a bareinstall-browserwould also fetch Firefox and WebKit). Verified on a cold directory:npm install --omit=devruns the hook and the browser launches. verify.js(30s) resolvesplaywright-corerelative to@playwright/mcp(it is a transitive dep, not declared by the connector's own package.json) and launches a real headless Chromium on about:blank — catches the two real failure modes (binary missing, system libraries missing on slim hosts) before the activation is saved.- Default tool set = 24 tools (no
--capsextras); all getdisplay_nameintools[]because@playwright/mcpdoes not emit MCPtitlefields intools/list(the "Title:" lines in its README are not on the wire). - ⚠️ Security: the default set includes
browser_run_code_unsafe(RCE-equivalent, arbitrary JS in the server process) andbrowser_evaluate(arbitrary JS in the page); there is no CLI flag to disable individual core tools. Consistent with themcp_localtrust model (guide §3) and declared in the manifest descriptions. - Icons: official Playwright SVG (Microsoft catalog), sized 48/96.
- Tested E2E reproducing skald's path (
npm install --omit=dev+node index.js):initialize,tools/list(24 tools), realbrowser_navigate+browser_snapshoton https://example.com ✅, verify probe ok ✅
Fixed
- playwright: default browser channel
chrome→ forced--browser chromium(v2 / 1.0.1) —@playwright/mcp0.0.79 defaults to thechromechannel, i.e. system Google Chrome: on slim Linux hosts (no/opt/google/chrome/chrome) every tool call failed withChromium distribution 'chrome' is not found at /opt/google/chrome/chrome. It went unnoticed in local testing because macOS borrowed the installed system Chrome, andverify.js(plainchromium.launch, no channel) probes the OSS build — not the one the server would actually launch. The wrapper now passes--browser chromium(the Chrome-for-Testing build downloaded by the postinstall hook, same as the official Docker image). Confirmed via the browser session registry:channel: chrome-for-testingafter the fix vschromebefore.
2026-08-10
Fixed
-
http-fetch + firecrawl:
npxlaunch was broken (v5 / 1.1.0) — both connectors declaredmcp_config: {command: "npx", args: ["-y", "<package>"]}and shipped no code files (onlyconnector.json+ icons).npx -y <package>is not expressible in skald. For atype: mcp_local, skald treatsargs[0]as the name of the file to run, not as an argument: at install it computesscript_path = "<id>/" + args[0]and clearsargs_json(marketplace.rs::install), thenglobal_enable(api/mcp.rs) resolves it to an absolute path and launches<command> <abs>. The real command becamenpx /…/connectors/http-fetch/-y— a nonexistent path, with-yand the package name lost. The process never answeredinitialize, sostart_serverfailed.- The failure was silent:
global_enablestill returns HTTP 200 with anerrorfield in the body, so the UI showed the connector as enabled while the runtime had no server. Andrender_mcp_list(loop_adapters/system.rs) builds the## MCP serverstable frommcp.tools(), i.e. the live runtime state, not the DB → the connector appeared activated and granted to the user but absent from the system context. ⚠️ This combination (200 +errorin the body) makes any connector that fails to start invisible in the UI: worth surfacing on the skald side. - Fix — two-file wrapper for both: a
package.jsonpinning the upstream package (mcp-fetch-server@1.1.2,firecrawl-mcp@3.23.7) and anindex.jsthat imports it for side effects (the module starts the JSON-RPC loop on stdio at import).mcp_configbecomes{command: "node", args: ["index.js"], transport: "stdio"}, i.e. a reallocal_script:ensure_installed_hostrunsnpm ci --omit=dev || npm install --omit=devin the connector folder before launch, exactly like whatsapp. Nonode_modulesshipped, no lockfile (like whatsapp). - Removed legacy fields
launch_command, top-leveltransport, anddependencies(dependenciesis only for the card, as already seen on gmaps;transportbelongs insidemcp_config). - firecrawl: removed
mcp_config.env: {"FIRECRAWL_API_KEY": "{SECRET:FIRECRAWL_API_KEY}"}— inert, same case as gmaps on 2026-08-10:apply_key_placeholdersubstitutes tokens only in the URL, never inenvvalues. It worked because the admin form sendsenvand that payload overwritesentry.env_json. - firecrawl: added
firecrawl_developer_searchtotools[](27 live tools vs 26 declared, verified on 3.23.7);requires["NODE"]→["NODE", "API_KEY"]. - Re-aligned manifest↔fragment versions to
5/1.1.0/2026-08-10for both: they were 2/1.0.1 (manifest) vs 4/1.0.3 (fragment), and skald prefers the manifest — soinstalled_versionstayed at 2 and the "Update available" badge would never have appeared. - Host requirement: these are
scope: globalconnectors, they run on the host, not in the container.mcp-fetch-serverwants Node ≥18,firecrawl-mcpwants Node ≥22. - Tested end-to-end reproducing skald's path (
npm ci || npm install+node <abs>/index.js):initialize,tools/list, and a realtools/call, stdout only JSON-RPC, clean stderr ✅ - Index regenerated with compile.py ✅
- ⚠️ Applying this to an instance that already has the connector installed is not just an Update.
refresh_connector_after_reinstall(skald/accessors.rs) updates only thedescriptionof themcp_global_serversrow, then restarts from that row:commandandargs_jsonstay the ones snapshotted at the firstglobal_enable, i.e. stillnpx+/…/connectors/<id>/-y. Procedure: deploy → Update from the marketplace (rewritesscript_pathin the catalog) → open the connector page and re-save the config, the only call that recomputescommand/argsand runsensure_installed_host. Same care already noted for gmaps. Whether the refresh should also re-derive command/args is open for evaluation on the skald side.
-
gmaps: missing dependency install + verify wired (v6 / 1.1.0)
- Added
requirements.txt(googlemaps>=4.10.0) — it was the only python connector without one. Dependencies were declared in the manifestdependenciesfield, which skald uses only for the card:ensure_installed_hostlooks exclusively atrequirements.txt/package.json. Result: empty.pydepsand logs full ofNo module named 'googlemaps', with the server still answeringtools/list(→connected — 6 tool(s)on a broken connector). - Wired the
verify(python3 verify.py, 20s):verify.pywas shipped but the manifest had noverifyblock, and skald readsverify_commandonly from there. Now an activation with broken dependencies fails visibly instead of starting silently. verify.pyputs.pydepsonsys.path: skald setsPYTHONPATHonly for the server process (global_row_spec), while verify runs assh -c "python3 verify.py"without it. Without this line, verify would fail with "Missing dependency" even on a correctly installed connector, disabling the row. ⚠️ Same latent risk for every connector with averifythat imports non-stdlib dependencies (gcal in container): to be checked.- Removed
mcp_config.env: {"GOOGLE_MAPS_API_KEY": "{SECRET:…}"}(introduced 2026-07-23): inert.apply_key_placeholdersubstitutes{SECRET:}/{ENV:}tokens only in the URL, never inenvvalues. It worked because the admin form sendsenvand that payload overwritesentry.env_json; with an empty form the process would have received the literal string. ⚠️ The spec in this file and in CLAUDE.md says the opposite — either fix the spec, or extend the substitution toenvvalues on the skald side. - Re-aligned manifest↔fragment versions to
6/1.1.0/2026-08-10: they were 2/1.0.1 vs 5/1.0.4, and skald prefers the manifest (manifest.version.or(entry.version)) — soinstalled_versionstayed at 2 and the "Update available" badge would never have appeared. requires:ENV→API_KEY; server error messages cleaned of references tosecrets/gmaps_api_key.txt(deprecated path).- Index regenerated with compile.py ✅
- Added
2026-08-07
Added
- New connector: LinkedIn (mcp_local, scope user): server.py + session.py + verify.py + PNG icons
- Added to
connectors/index.json, index regenerated with compile.py (17 connectors total) .gitignoreupdated to ignore.claude/- Deploy to connectors.skaldagent.net via
skaldserveralias (192.168.1.100, LAN — no Tailscale)
2026-07-23
Changed
- gmaps: env var injection fix — added
mcp_config.envinconnector.jsonto injectGOOGLE_MAPS_API_KEYinto the MCP process. The connector was declared asdelivery: envbut withoutmcp_config.envSkald could not pass the variable to the Python process. Version bump: fragment 4→5, connector 1→2. Index regenerated with compile.py ✅ - Context7 icon update (PNG) — replaced Context7 icons from SVG to PNG (new icon provided by the user):
icon_sm.png— 48×48 (2.7 KB),icon_lg.png— 96×96 (4.5 KB). Old SVGs removed, references updated in fragment.json and connector.json. Version bump: fragment 3→4, connector 1→2. Index regenerated with compile.py ✅ - SerpAPI Flights icon update (PNG) — replaced SerpAPI Flights icons from SVG to PNG:
icon_sm.png— 48×48 (2.9 KB),icon_lg.png— 96×96 (6.8 KB). Old SVGs removed, references updated in fragment.json and connector.json. Version bump: fragment 4→5, connector 2→3. Index regenerated with compile.py ✅