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

82 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Changelog
All notable changes to the Skald Connectors Marketplace are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0/).
## [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`: `ENV``API_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 ✅