Files
skald-connectors/CHANGELOG.md
T
Daniele 78c78d6039 docs: merge SKALD.md into CLAUDE.md and remove it
CLAUDE.md and SKALD.md had drifted apart. CLAUDE.md still described a
hand-maintained connectors.json (pre-compile.py), an rsync deploy, 5
connectors, and a files[] that excluded connector.json.

CLAUDE.md is now the single working document, carrying every SKALD.md
section — full schemas for connectors.json / fragment.json /
connector.json, reserved enums, the auth/deliver/env/verify fields,
placeholder syntax, friendly tool names, icon conventions, file
integrity, local workflow and deploy — corrected against the actual
repo state: 18 connectors, the scripts/compile.py pipeline, and
connector.json included in the hashed files[].

It also names CONNECTOR_MANIFEST_GUIDE.md (repo root) as the
authoritative connector-authoring spec.
2026-08-24 17:37:20 +01:00

29 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]

Changed

  • Docs: SKALD.md merged into CLAUDE.md and removed. The two files had drifted apart — CLAUDE.md still described a hand-maintained connectors.json (pre-compile.py), an rsync deploy, 5 connectors, and a files[] that excluded connector.json. CLAUDE.md is now the single authoritative spec, carrying every SKALD.md section (full schemas for connectors.json / fragment.json / connector.json, reserved enums, auth/deliver/env/verify fields, placeholder syntax, icon conventions, file integrity, local workflow, deploy) corrected against the actual repo state: 18 connectors, the compile.py pipeline, and connector.json included in the hashed files[]. CLAUDE.md now also names CONNECTOR_MANIFEST_GUIDE.md (repo root) as the authoritative connector-authoring spec.

2026-08-24

Fixed

  • google-trends: the MCP server never completed a handshake (v2 / 1.1.0) — the connector was unusable since it shipped. handle_request implemented exactly two methods, tools/list and tools/call; everything else fell through to -32601 Method not found. Since initialize is the first message any MCP client sends, the client got an error to its opening request and aborted before a single tool could be listed. Three further protocol defects sat behind it:

    • tools/list returned a bare array instead of the {"tools": [...]} object the spec requires — so even a client that tolerated the missing handshake would have parsed zero tools.
    • No notifications/initialized, no ping. The former is the notification a client sends immediately after initialize; the latter is the standard liveness probe.
    • Notifications got answered. Any message without an id (a notification, by definition) still produced a full JSON-RPC response written to stdout — a protocol violation that desynchronises a strict client. handle_request now returns None for every id-less message.
    • Fix: the server now mirrors the shape every other local connector in this repo already uses (wikipedia, weather, gmaps): initializeprotocolVersion 2024-11-05 + serverInfo, silent notifications/initialized, ping{}, tools/list{"tools": TOOLS}, and a TOOLS manifest list + TOOL_DISPATCH map replacing the old dict-of-tuples and the title_map that was rebuilt inside the request handler on every call.
    • Verified end-to-end by piping a real handshake into the process: initializetools/listtools/call for each tool, plus a notification and an unknown method
  • google-trends: include_articles: true always returned zero articles — a silent data bug independent of the handshake. The RSS mapper read t.get("articles"), but trendspyg emits the key as news_articles. The lookup never matched, so the field came back as an empty list for every trend and the caller had no way to tell "no articles" from "wrong key". Now reads news_articles; explore_link (the trend's Google Trends URL, previously discarded) is included too.

  • google-trends: a malformed tool argument crashed the call as a protocol error — tools were invoked as fn(**arguments) against typed keyword parameters, so an unexpected key or a string where a number belonged raised TypeError and surfaced as -32603 Internal error, which reads to the agent as a broken server rather than a bad argument. Handlers now take a single args: dict and coerce through _str_arg / _int_arg / _bool_arg (clamping max_trends to 1-20 instead of resetting a negative value to 10).

Changed

  • google-trends: tool failures are now marked as failures. Errors were returned as {"status": "error", ...} inside a successful result — structurally indistinguishable from data. They now follow the repo convention: an Error: … text result carrying isError: true. trendspyg's typed exceptions are translated into actionable messages (RateLimitError → retry later, BrowserError → Chrome missing, InvalidParameterError → bad input) instead of a bare str(e).
  • google-trends: browser calls now fail fast. explore and get_interest_over_time used trendspyg's defaults of 10 retries × 8s, allowing a ~100s call — well past any agent's tool timeout. Capped at 3 × 6.0s (~25s worst case), matching the "~10-25s" the tool descriptions promise.
  • google-trends: trendspyg pinned to >=1.6.0 (was >=0.7.0) and imported defensively — an import failure no longer kills the process at startup, so the handshake still succeeds and the missing dependency is reported as a readable tool error.
  • google-trends: verify is now actually wired. verify.py shipped in files[] since day one but connector.json declared no verify block, so skald never ran it. Added (python3 verify.py, 20s), and the script was upgraded from a bare import trendspyg check to a real RSS fetch against Google Trends that fails if zero trends come back.
  • google-trends: explore no longer mutates trendspyg's envelope. It injected data["status"] = "ok" into the returned ExploreEnvelope, assuming the return was always a dict. The envelope is now passed through untouched.
  • google-trends: dropped the output_format parameter from get_interest_over_time. It was exposed in the input schema but was a no-op for the caller — the "json" branch immediately re-parsed the string back into the same dict the "dict" branch produced.
  • google-trends: tool descriptions rewritten to state what each tool is for and when to prefer one over another (explore over get_interest_over_time when related queries or the regional breakdown are wanted), and that get_trending is always-current and cannot look at past dates.

2026-08-23

Added

  • whatsapp: chats and messages now survive a restart (v8 / 2.2.0) — closes the gap left open by v7. WhatsApp delivers a history sync at login, not on reconnect, so with a purely in-memory store every process restart left the connector blind until new messages happened to arrive — and the process had restarted 23 times in two weeks. The store is now mirrored to store/, next to auth/ and bind-mounted the same way, so it outlives both a restart and a container recreate.
    • No SQLite, deliberately. node:sqlite needs Node 22 (and is only unflagged from Node 24); the runtime image ships Debian trixie's nodejs = 20.19.2. better-sqlite3 is a native module the slim image has no toolchain to build. So: store/messages.jsonl, an append-only log (one appendFileSync per message, O(1)), plus store/meta.json, a debounced snapshot of chats/contacts (5s, since they churn in bursts during a history sync). Both written via write-tmp-then-rename, which is atomic — a crash mid-write cannot truncate the store.
    • The log is compacted on load and every 500 appends: the capped in-memory Maps are re-serialised, so the file cannot creep upward across restarts.
    • Fixes a pre-existing duplication bug in the process. pushMessage appended unconditionally, so every history re-sync re-added messages already held. It now returns false on a known message id (a ≤500-element scan per message) and ingestMessage skips both the transcript and the log — without it, persistence would have multiplied the duplicates once per restart instead of merely showing them.
    • MAX_MSGS_PER_CHAT 200 → 500, worth more now that it is not thrown away at every restart. logout deletes store/ along with auth/, so re-linking a different phone cannot inherit the previous account's history.
    • ⚠️ Data at rest: message text is now written to disk in the user's bind-mounted home. Nothing was persisted before this change beyond the session keys.
    • Verified on skald-runtime:v4 with a seeded store — 751 log lines containing 50 exact duplicates, a 700-message chat, and a deliberately torn trailing line → loaded as 701 messages (duplicates collapsed, torn line skipped, no crash), compacted to 501 lines, the 700-message chat trimmed to its most recent 500, meta.json round-tripped, and a second run reloading 501 → 501 unchanged

Fixed

  • whatsapp: history sync silently disabled, and Signal sessions corrupted by reconnects (v7 / 2.1.0) — diagnosed from the live server, where two users (two separate accounts, separate auth/ dirs) share one log file. Four distinct defects, plus a dependency upgrade.
    • The connector processed no history at all on Baileys 6.7.x. Socket/index.js derives the history gate from the sync flag when the caller leaves it unset: if (config.shouldSyncHistoryMessage === undefined) newConfig.shouldSyncHistoryMessage = () => !!newConfig.syncFullHistory;. The connector passed neither, and shipped syncFullHistory: false — so the gate evaluated to () => false, shouldProcessHistoryMsg was permanently false, and the history-sync blob was discarded. list_chats / get_messages only ever saw messages arriving live after startup, which is what "no chats known yet" really meant. The derivation does not exist in 6.17.x or 7.x, where the default is () => true — so the same connector behaved differently per user depending on which version npm had resolved.
    • Fix: shouldSyncHistoryMessage: () => true is now passed explicitly, making behaviour identical on every Baileys version, and syncFullHistory: true. The browser identity had to change too: getWebInfo only requests a desktop-grade sync when browser[0] is 'Mac OS' or 'Windows' (PLATFORM_MAP has exactly those two keys) — with the old ['Skald', 'Chrome', …] the sub-platform stayed WEB_BROWSER and syncFullHistory would have been inert. Now ['Mac OS', 'Chrome', '121.0.0']; the trade-off is that the phone lists the device as "Mac OS Chrome" rather than "Skald".
    • Every reconnect leaked its socket, giving auth/ two concurrent writers. The connection === 'close' branch only set starting = false and re-entered startSock(): no end(), no removeAllListeners(). Each pass called useMultiFileAuthState(AUTH_DIR) again, building a fresh key cache, while the previous socket stayed alive with its own creds.update → saveCreds handler bound to the old snapshot. At ~15 reconnects/day (106× 428 connectionClosed, 42× 500 badSession, 34× 503, 9× 405 over two weeks) that is the standard route to inconsistent Signal state — and the logs showed the symptom: 1422 Bad MAC lines and 51× Closing open session in favor of incoming prekey bundle. Every failing session address was the account's own LID at device 0 (its own phone): the own-device sync stream, not a remote contact.
    • Fix: teardownSock() detaches listeners and closes the socket before a new one is built, each socket carries a generation number so events from a superseded socket are dropped, and reconnects go through a single-slot timer with exponential backoff (1.5s → 60s, reset on open) instead of a fixed 1.5s re-entry. Disconnects are now logged by name (428 connectionClosed) rather than bare code.
    • Undecryptable messages were stored as blank lines. A message Signal cannot open arrives with no message payload and messageStubType = CIPHERTEXT (2); textOf() returned '' and ingestMessage stored it anyway, so get_messages rendered [timestamp] name: and the agent could not distinguish a silent gap from an empty message. They are now labelled [undecryptable message], counted, and surfaced in status. Text-less protocol frames (reactions, receipts, key distribution) are dropped instead of padding transcripts with blanks.
    • fetchLatestBaileysVersion() ran on every reconnect — ~15 outbound calls/day, each able to hand the socket a protocol version the installed library cannot speak (the plausible source of the 9× 405 closes). Now fetched once and cached for 6h, falling back to the last good value, then to the library default.
    • Log noise made the shared file unreadable. libsignal bypasses the Baileys logger and calls console.error directly — one Failed to decrypt… line plus a full stack trace per candidate session — which is how ~30 real failures became 1422 lines. console.error is now wrapped to collapse the burst into one counted line, and every connector line carries the linked account ([whatsapp_mcp Daniele]) so the two users' entries are separable. console.log/info/warn are redirected to stderr as well: stdout is the JSON-RPC channel and a dependency printing there would corrupt the framing.

Changed

  • whatsapp: Baileys ^6.7.9 → pinned 7.0.0-rc14, connector converted to ESM (v7 / 2.1.0) — the caret range was resolving to different versions per install: 6.7.24 for one user, 6.17.16 for the other (that version sorts above 6.7.24 by semver but sits outside the maintained 6.7.x line; npm's legacy dist-tag points at 6.7.24, latest at 7.0.0-rc14). The user on 6.17.16 logged 681 decrypt failures against the other's 30. Both @whiskeysockets/baileys and qrcode are now pinned exactly, matching the convention already used by playwright/http-fetch/firecrawl.
    • 7.0.0-rc14 is a release candidate — there is no stable 7.x — chosen deliberately: its defaults already do the right thing (syncFullHistory: true, no derived history gate) and the API surface the connector uses is unchanged (makeWASocket, useMultiFileAuthState, DisconnectReason, fetchLatestBaileysVersion, jidNormalizedUser, plus WAMessageStubType).
    • It ships ESM-only ("type": "module", engines node >= 20), so index.js moved from CommonJS to ESM rather than leaning on Node's require(esm) bridge; __dirname is derived from import.meta.url. package.json gains "type": "module" and engines.node >= 20 (the skald-runtime:v4 image runs 20.19.2).
    • Verified on the real runtime image, not locally: ESM import resolves under Node 20.19.2, initialize + tools/list (7 tools) + status + list_chats answer correctly, protocol version fetch and QR generation work, and the logout path exercises teardown → single reconnect → fresh QR with no duplicate socket
    • Re-aligned manifest↔fragment versions to 7 / 2.1.0 (they had drifted to 3/2.0.3 vs 6/2.0.6; skald reads installed_version from the manifest, so the update badge would not have appeared).
    • ⚠️ Still outstanding: the chat/contact/message store is in-memory only. History sync arrives at login, not on every reconnect, so whatever the full sync delivers is lost at the next process restart. Persisting the store is the remaining piece and was left out of this change.

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