Files
skald-connectors/CHANGELOG.md
T
Daniele 040a0b2320 exa: fix verify.py rejecting every API key (v5 / 1.0.4)
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.
2026-08-20 23:02:30 +01:00

14 KiB
Raw Blame History

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.py rejected every API key, valid or not (v5 / 1.0.4) — activating Exa always failed with Exa 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.ai sits behind Cloudflare, which bans urllib's default Python-urllib/3.x agent with HTTP 403 / error code: 1010 (browser-signature ban). The script sent no User-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 initialize against 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 the x-api-key header, 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 MCP initialize and reports reachability only, never validity. Both requests now send a User-Agent, and the MCP one also sends Accept: 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 reads installed_version from the manifest, so the update badge would never have appeared). verify.timeout_secs 15 → 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 real web_search_exa call against https://mcp.exa.ai/mcp?exaApiKey=… returned results
    • ⚠️ Same latent pattern elsewhere: context7, tavily, and serpapi-flights also probe over urllib with no User-Agent and 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.

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.json pins the package and index.js rewrites argv (--headless --isolated --no-sandbox, plus a process.argv.slice(2) passthrough) and imports the package's cli.js, which self-executes at import time. cli.js is not in the package exports map, so the wrapper resolves it from the exported package.json path.
    • 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 playwright package does NOT download browsers at npm install (verified on 1.63.0-alpha-2026-08-05), and @playwright/mcp 0.0.79 no longer ships the old browser_install tool (the README section is now empty). So the connector's package.json carries a postinstall hook — node node_modules/@playwright/mcp/cli.js install-browser chromium — which downloads Chromium + headless shell + ffmpeg only (~350 MB; a bare install-browser would also fetch Firefox and WebKit). Verified on a cold directory: npm install --omit=dev runs the hook and the browser launches.
    • verify.js (30s) resolves playwright-core relative 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 --caps extras); all get display_name in tools[] because @playwright/mcp does not emit MCP title fields in tools/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) and browser_evaluate (arbitrary JS in the page); there is no CLI flag to disable individual core tools. Consistent with the mcp_local trust 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), real browser_navigate + browser_snapshot on https://example.com , verify probe ok

Fixed

  • playwright: default browser channel chrome → forced --browser chromium (v2 / 1.0.1)@playwright/mcp 0.0.79 defaults to the chrome channel, i.e. system Google Chrome: on slim Linux hosts (no /opt/google/chrome/chrome) every tool call failed with Chromium distribution 'chrome' is not found at /opt/google/chrome/chrome. It went unnoticed in local testing because macOS borrowed the installed system Chrome, and verify.js (plain chromium.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-testing after the fix vs chrome before.

2026-08-10

Fixed

  • http-fetch + firecrawl: npx launch was broken (v5 / 1.1.0) — both connectors declared mcp_config: {command: "npx", args: ["-y", "<package>"]} and shipped no code files (only connector.json + icons).

    • npx -y <package> is not expressible in skald. For a type: mcp_local, skald treats args[0] as the name of the file to run, not as an argument: at install it computes script_path = "<id>/" + args[0] and clears args_json (marketplace.rs::install), then global_enable (api/mcp.rs) resolves it to an absolute path and launches <command> <abs>. The real command became npx /…/connectors/http-fetch/-y — a nonexistent path, with -y and the package name lost. The process never answered initialize, so start_server failed.
    • The failure was silent: global_enable still returns HTTP 200 with an error field in the body, so the UI showed the connector as enabled while the runtime had no server. And render_mcp_list (loop_adapters/system.rs) builds the ## MCP servers table from mcp.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 + error in 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.json pinning the upstream package (mcp-fetch-server@1.1.2, firecrawl-mcp@3.23.7) and an index.js that imports it for side effects (the module starts the JSON-RPC loop on stdio at import). mcp_config becomes {command: "node", args: ["index.js"], transport: "stdio"}, i.e. a real local_script: ensure_installed_host runs npm ci --omit=dev || npm install --omit=dev in the connector folder before launch, exactly like whatsapp. No node_modules shipped, no lockfile (like whatsapp).
    • Removed legacy fields launch_command, top-level transport, and dependencies (dependencies is only for the card, as already seen on gmaps; transport belongs inside mcp_config).
    • firecrawl: removed mcp_config.env: {"FIRECRAWL_API_KEY": "{SECRET:FIRECRAWL_API_KEY}"} — inert, same case as gmaps on 2026-08-10: apply_key_placeholder substitutes tokens only in the URL, never in env values. It worked because the admin form sends env and that payload overwrites entry.env_json.
    • firecrawl: added firecrawl_developer_search to tools[] (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-10 for both: they were 2/1.0.1 (manifest) vs 4/1.0.3 (fragment), and skald prefers the manifest — so installed_version stayed at 2 and the "Update available" badge would never have appeared.
    • Host requirement: these are scope: global connectors, they run on the host, not in the container. mcp-fetch-server wants Node ≥18, firecrawl-mcp wants Node ≥22.
    • Tested end-to-end reproducing skald's path (npm ci || npm install + node <abs>/index.js): initialize, tools/list, and a real tools/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 the description of the mcp_global_servers row, then restarts from that row: command and args_json stay the ones snapshotted at the first global_enable, i.e. still npx + /…/connectors/<id>/-y. Procedure: deploy → Update from the marketplace (rewrites script_path in the catalog) → open the connector page and re-save the config, the only call that recomputes command/args and runs ensure_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 manifest dependencies field, which skald uses only for the card: ensure_installed_host looks exclusively at requirements.txt / package.json. Result: empty .pydeps and logs full of No module named 'googlemaps', with the server still answering tools/list (→ connected — 6 tool(s) on a broken connector).
    • Wired the verify (python3 verify.py, 20s): verify.py was shipped but the manifest had no verify block, and skald reads verify_command only from there. Now an activation with broken dependencies fails visibly instead of starting silently.
    • verify.py puts .pydeps on sys.path: skald sets PYTHONPATH only for the server process (global_row_spec), while verify runs as sh -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 a verify that 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_placeholder substitutes {SECRET:}/{ENV:} tokens only in the URL, never in env values. It worked because the admin form sends env and that payload overwrites entry.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 to env values 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)) — so installed_version stayed at 2 and the "Update available" badge would never have appeared.
    • requires: ENVAPI_KEY; server error messages cleaned of references to secrets/gmaps_api_key.txt (deprecated path).
    • Index regenerated with compile.py

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)
  • .gitignore updated to ignore .claude/
  • Deploy to connectors.skaldagent.net via skaldserver alias (192.168.1.100, LAN — no Tailscale)

2026-07-23

Changed

  • gmaps: env var injection fix — added mcp_config.env in connector.json to inject GOOGLE_MAPS_API_KEY into the MCP process. The connector was declared as delivery: env but without mcp_config.env Skald 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