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.
82 lines
14 KiB
Markdown
82 lines
14 KiB
Markdown
# 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 ✅
|