From 78c78d60392c462bf5e04eb3d2d111f4a3eac96a Mon Sep 17 00:00:00 2001 From: Daniele Date: Mon, 24 Aug 2026 17:37:20 +0100 Subject: [PATCH] docs: merge SKALD.md into CLAUDE.md and remove it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- CHANGELOG.md | 4 + CLAUDE.md | 647 ++++++++++++++++++++++++++++++++++++++++++++++----- SKALD.md | 543 ------------------------------------------ 3 files changed, 598 insertions(+), 596 deletions(-) delete mode 100644 SKALD.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 64be13c..a1d2a46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [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 diff --git a/CLAUDE.md b/CLAUDE.md index c878148..7d60689 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,86 +1,627 @@ # CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +It documents the repo layout, the schemas, and the build/deploy workflow — read it before changing any +manifest. The **authoritative spec** for authoring a connector lives in this repo as +[CONNECTOR_MANIFEST_GUIDE.md](CONNECTOR_MANIFEST_GUIDE.md) (see § The spec). ## What this is -The **Skald Connectors Marketplace** — a catalog of tested connectors (adapters that let Skald agents talk to external services). It is **not an application**: there is no build system, package manager, or test suite. The repo is a set of JSON manifests, static HTML, icons, and standalone Python MCP scripts that are served as-is over HTTP. +The **Skald Connectors Marketplace** — the catalog of tested connectors for Skald. Each connector is an +adapter that lets a Skald agent talk to an external service (API, email, calendar, search, messaging…). -`SKALD.md` is the authoritative spec — read it before changing any schema or manifest. +It is **not an application**: there is no build system, package manager, or test suite. The repo is a set +of JSON manifests, static HTML, icons, and standalone MCP scripts (Python/Node) served as-is over HTTP. +The only tooling is `scripts/compile.py`, which regenerates the index. + +- **Remote git**: `https://git.skaldagent.net/dguiducci/skald-connectors.git` (branch `main`) +- **Live site**: `https://connectors.skaldagent.net/` +- **OAuth callback**: `https://connectors.skaldagent.net/oauth/show.html` + +`main` is the **release** branch — only production-ready code lands here. Development/alpha versions will +live on separate branches in the future. + +### The spec + +The marketplace spec ships **inside this project**: [CONNECTOR_MANIFEST_GUIDE.md](CONNECTOR_MANIFEST_GUIDE.md) +at the repo root is the authoritative, step-by-step specification for producing a correct connector — +the two documents, the MCP-over-stdio server contract, friendly tool names, placement & risk vocabulary, +the `auth.type` variants (`api_key` / `oauth2` / `qr`), dependency installation, verify-before-save, +versioning, and the new-connector checklist. Give this file to any agent that generates new connectors, +and update it whenever the connector format changes. + +Consumers outside this repo (e.g. `~/projects/skald-circle`) read that same file as the format reference. + +[docs/connector.manifest_guide.md](docs/connector.manifest_guide.md) is an earlier copy of the same guide; +it is the only one that documents the `fragment.json` + `scripts/compile.py` build pipeline, which +`CONNECTOR_MANIFEST_GUIDE.md` does not yet cover. Until the root guide absorbs that section, the compile +workflow is described below in § Architecture and § Local workflow. ## Architecture -Two-tier manifest model with a single trust root: +Two-tier manifest model with a single trust root, plus a compile step: -- **`connectors/connectors.json`** — the **index** and single root of trust. Lists every connector and, per connector, a `files[]` array with the `sha256` + `size` of each shipped file. It has **no hash of itself**, and the `files[]` arrays **do not include `connector.json`** — both are treated as unsigned manifests. Intended to be digitally signed in the future. -- **`connectors//connector.json`** — per-connector **technical manifest** for activation (launch command, transport, `mcp_config`, `auth`, `dependencies`, `docs`). One folder per connector; folder name matches the connector `id`. -- **`connectors/index.html`** — catalog UI. Fetches `/connectors.json` at absolute path and links to `//`, so it only works when served from the site root (not opened as a `file://`). +- **`connectors/index.json`** — ordered list of connector ids. The input list for `compile.py`. +- **`connectors//fragment.json`** — the connector's **index entry without `files[]`** (id, name, type, + scope, icons, `user_description`, requires, tags, version, `tools[]`, auth…). +- **`connectors//connector.json`** — per-connector **technical manifest** used at activation time + (launch command, transport, `mcp_config`, `auth`, `env`, `verify`, `dependencies`, `docs`). One folder + per connector; the folder name matches the connector `id`. +- **`connectors/connectors.json`** — the **compiled index** and single root of trust. Generated by + `scripts/compile.py` from `index.json` + each `fragment.json` + a physical scan of every connector + folder. It lists every connector with a `files[]` array carrying the `sha256` + `size` of each shipped + file. **Never edit it by hand.** It carries no hash of itself — in the future it may be digitally signed. +- **`connectors/index.html`** — catalog UI. Fetches `/connectors.json` at an absolute path and links to + `//`, so it only works when served from the site root (not opened as a `file://`). +- **`connectors/oauth/show.html`** — OAuth callback receiver. -Connector taxonomy (see SKALD.md for full enums): -- `type`: `mcp_remote` (hosted MCP server reached by URL, e.g. Tavily) vs `mcp_local` (script run client-side, e.g. Gmail). -- `scope`: `global` (one shared config) vs `user` (per-user auth/instance). -- `requires`: prerequisites like `API_KEY`, `OAUTH`, `PYTHON`, `NODE`, `DOCKER`, `ENV`. (`SECRETS_DIR` is **deprecated** — the `secrets/` folder is gone from the model; connectors must declare their inputs as `ENV`/`SECRET`.) - -**Unified placeholder syntax.** Every value skald must fill at runtime uses one of two tokens, anywhere it appears (URL, `mcp_config.env`, `verify.command`): -- `{ENV:NAME}` — a non-sensitive value (host, port, username…), declared in the top-level `env[]` array. -- `{SECRET:NAME}` — a sensitive value (password, API key, token), also declared in `env[]` with `secret: true`. - -Legacy tokens `{key}`, `{env:NAME}`, `{secrets}/…` are **deprecated** and skald no longer substitutes them. The api-key of an `auth.type = "api_key"` connector is exposed as `{SECRET:}` (e.g. Tavily: `?tavilyApiKey={SECRET:tavilyApiKey}`). See SKALD.md § Placeholder syntax. - -**Config via `env[]` (`env` manifest field):** a connector whose `requires` includes `ENV` declares its required environment variables in a top-level `env` array in `connector.json` — each entry has `name`, `label`, `description`, `required`, `secret`, and optional `default`/`example`. skald renders a real form from this schema (masking `secret: true` fields, marking `required`, using `example` as placeholder) and injects the collected values into the process environment at launch; the server reads `os.environ`. Nothing is written to disk. The `email` connector is the reference example (see SKALD.md § The env field). **`tavily` now also declares `env[]`** (its API key as a `secret` field). - -**Verify-before-save (`verify` manifest field).** A connector may declare a `verify` step — a shell command skald runs *after* the user fills the form and *before* persisting the activation, to confirm the credentials actually work: -```json -"verify": { "command": "python3 verify.py", "timeout_secs": 20 } ``` -The command runs in the same sandbox as the server (container `skald-{userid}` for `mcp_local` user, host for `mcp_remote` global), with the collected env/secret injected. It must print one JSON object on stdout — `{"ok": bool, "message": string, "details"?: object}` — and exit 0 on success. If `verify` is absent, no test runs and the activation is direct. See SKALD.md § The verify field. `email` and `tavily` ship a `verify.py`; `gmail` will get one in the OAuth phase. +connectors/ +├── index.json ← ordered list of connector ids (input for compile.py) +├── connectors.json ← COMPILED INDEX (generated by compile.py, do not edit) +├── index.html ← catalog UI (reads connectors.json via fetch) +├── oauth/ +│ └── show.html ← OAuth callback receiver +├── gmail/ ← one connector per folder +│ ├── fragment.json ← index fragment (id, name, type, …, WITHOUT files[]) +│ ├── connector.json ← technical configuration (mcp_config, auth.deliver, …) +│ ├── gmail_mcp_server.py ← MCP script +│ ├── requirements.txt ← Python dependencies +│ ├── icon_sm.svg ← small icon (~48×48) +│ └── icon_lg.svg ← large icon (~96×96) +├── email/ +│ ├── fragment.json +│ ├── connector.json +│ ├── email_mcp_server.py +│ ├── verify.py +│ ├── icon_sm.svg +│ └── icon_lg.svg +└── … +``` -**MCP servers** (`connectors/gmail/gmail_mcp_server.py`, `connectors/email/email_mcp_server.py`) are hand-rolled JSON-RPC 2.0 servers over stdio — no MCP SDK / FastMCP. Shared conventions: **stdout is reserved for JSON-RPC**, all logging goes to stderr, a lock guards stdout writes, a background thread emits `event/new_email` push notifications, and each server exposes the same tool shape (`TOOLS` manifest + `TOOL_DISPATCH` + `handle_request`). Mirror this structure for new local connectors. -- **Gmail** uses the Gmail API with OAuth; today it still reads its token from `./secrets/gmail_creds.json` (override `GMAIL_CREDS_PATH`) generated by `gmail_oauth_setup.py`. The `secrets/` path is **deprecated** — the OAuth phase (skald Fase 2) will migrate Gmail to `{ENV:}`/`{SECRET:}` and a loopback-listener flow. Push = History API polling. `verify` is not yet wired for Gmail. -- **Email** is a generic IMAP+SMTP connector, **stdlib-only (no dependencies)**, configured entirely from `{ENV:}`/`{SECRET:}` env vars (no `secrets/`), works with any provider. Push = IMAP IDLE with a 60s polling fallback; the request thread and the watcher thread each hold their own IMAP connection (imaplib is not thread-safe). Note: imaplib does not quote SEARCH arguments, so values with spaces must be wrapped via `_q()`. Ships a `verify.py` (IMAP+SMTP login probe). -- **Tavily** is `mcp_remote`/`global`; its API key is declared as an `env[]` secret (`tavilyApiKey`) and templated into the URL as `{SECRET:tavilyApiKey}`. Ships a `verify.py` (minimal `/search` POST probe). -- **SSH** is `mcp_local`/`user`; stores aliases in `~/.ssh_aliases.json` (auto-managed, 0600). Auth per-alias: key/agent (default) or elicited password. Sudo via `nopasswd` or elicited password (`sudo -S`). No setup-time credentials: `auth.type: "none"` with no `verify` (activation is direct). All env vars (`SSH_MCP_*`) are optional with defaults. Ships `requirements.txt` (paramiko). +## Schema — `connectors.json` (compiled root index) + +```json +{ + "version": 1, + "connectors": [ + { + "id": "gmail", + "name": "Gmail", + "type": "mcp_local", + "scope": "user", + "icon_small": "gmail/icon_sm.svg", + "icon_large": "gmail/icon_lg.svg", + "user_description": "Read, send, and manage Gmail emails via OAuth…", + "requires": ["OAUTH", "PYTHON"], + "tags": ["email", "mcp", "local", "google"], + "auth": { + "type": "oauth2", + "provider": "google", + "scopes": [ + "https://www.googleapis.com/auth/gmail.modify", + "https://www.googleapis.com/auth/gmail.labels" + ] + }, + "folder": "gmail", + "version": 1, + "version_string": "1.0.0", + "version_release_date": "2026-07-19", + "files": [ + {"path": "connector.json", "sha256": "…", "size": 1204}, + {"path": "gmail_mcp_server.py", "sha256": "…", "size": 46772}, + {"path": "icon_lg.svg", "sha256": "…", "size": 254}, + {"path": "icon_sm.svg", "sha256": "…", "size": 251}, + {"path": "requirements.txt", "sha256": "…", "size": 82} + ] + } + ] +} +``` + +### Index fields + +| Field | Required | Description | +|-------|----------|-------------| +| `id` | ✅ | Unique identifier (kebab-case) | +| `name` | ✅ | Displayed name | +| `type` | ✅ | `mcp_remote` or `mcp_local` | +| `scope` | ✅ | `global` or `user` | +| `icon_small` | ✅ | Path relative to the marketplace root | +| `icon_large` | ✅ | Path relative to the marketplace root | +| `user_description` | ✅ | Short description for the UI | +| `requires` | ✅ | Array of requirement enums | +| `tags` | ✅ | Array of tags for filtering | +| `folder` | ✅ | Name of the connector folder | +| `version` | ✅ | Per-connector integer, +1 on every file change | +| `version_string` | ✅ | Semver (display only) | +| `version_release_date` | ✅ | ISO 8601 date `YYYY-MM-DD` (display only) | +| `files` | ✅ | Array of `{path, sha256, size}` — added automatically by `compile.py`, NO self-hash | + +## Schema — `fragment.json` (per folder) + +`fragment.json` contains all the fields of a `connectors.json` entry **except** `files[]`: + +| Field | Required | Description | +|-------|----------|-------------| +| `id` | ✅ | Unique identifier (matches the folder name) | +| `name` | ✅ | Displayed name | +| `type` | ✅ | `mcp_remote` or `mcp_local` | +| `scope` | ✅ | `global` or `user` | +| `icon_small` | ✅ | Path relative to the marketplace root | +| `icon_large` | ✅ | Path relative to the marketplace root | +| `user_description` | ✅ | Short description for the UI | +| `requires` | ✅ | Array of requirement enums | +| `tags` | ✅ | Array of tags for filtering | +| `folder` | ✅ | Name of the connector folder (matches `id`) | +| `version` | ✅ | Per-connector integer, +1 on every file change | +| `version_string` | ✅ | Semver (display only) | +| `version_release_date` | ✅ | ISO 8601 date (display only) | +| `tools` | ✅ | Array of `{name, display_name}` for friendly UI names | +| `auth` | optional | Authentication configuration (if other than `"none"`) | + +The `files[]` array is added **automatically** by `compile.py` from the files present in the folder — it +must never be written by hand. + +## Schema — `connector.json` (per folder) + +Technical configuration for connector activation. + +```json +{ + "id": "gmail", + "name": "Gmail", + "version": 1, + "version_string": "1.0.0", + "version_release_date": "2026-07-19", + "type": "mcp_local", + "scope": "user", + "launch_command": "python3 gmail_mcp_server.py", + "transport": "stdio", + "requires": ["OAUTH", "PYTHON"], + "tags": ["email", "mcp", "local", "google"], + "dependencies": [ + "google-api-python-client>=2.150.0", + "google-auth>=2.35.0", + "google-auth-oauthlib>=1.2.0" + ], + "setup_instructions": [ + "Install dependencies: pip install -r requirements.txt" + ], + "docs": [ + { + "lang": "en", + "description": "Full description for human users…", + "llm_short_description": "Gmail — read, send, label, and search email. Supports push notifications." + } + ], + "auth": { + "type": "oauth2", + "provider": "google", + "scopes": [ + "https://www.googleapis.com/auth/gmail.modify", + "https://www.googleapis.com/auth/gmail.labels" + ], + "deliver": { + "as": "env", + "format": "google_authorized_user", + "env": "GMAIL_CREDS_JSON" + } + }, + "mcp_config": { + "command": "python3", + "args": ["gmail_mcp_server.py"] + }, + "homepage": "https://mail.google.com", + "icon_small": "icon_sm.svg", + "icon_large": "icon_lg.svg" +} +``` + +### `connector.json` fields + +| Field | Required | Description | +|-------|----------|-------------| +| `id` | ✅ | Unique identifier (matches the folder name) | +| `name` | ✅ | Displayed name | +| `version` | ✅ | Per-connector integer, +1 on every file change | +| `version_string` | ✅ | Semver (display only) | +| `version_release_date` | ✅ | ISO 8601 date (display only) | +| `type` | ✅ | `mcp_remote` or `mcp_local` | +| `scope` | ✅ | `global` or `user` | +| `requires` | ✅ | Array of requirement enums | +| `tags` | ✅ | Array of tags | +| `auth` | ✅ | Authentication configuration object | +| `docs` | ✅ | Array of multilingual documentation. **`llm_short_description`** is the field that ends up in the LLM's system prompt — it must describe WHAT the connector DOES, not list its tools (the LLM sees them after `activate_tools`). Example: *"Weather — current conditions, 16-day forecast, and AQI data for any location."* | +| `icon_small` | ✅ | Icon filename in the local folder | +| `icon_large` | ✅ | Icon filename in the local folder | +| `launch_command` | `mcp_local` only | Command to start the MCP server | +| `transport` | `mcp_local` only | `stdio` (default) | +| `dependencies` | recommended | Python/Node dependencies (empty array if stdlib only) | +| `env` | if the connector needs user-supplied config | Environment variables the user must provide (schema for the UI) — see § The env field | +| `verify` | recommended | Test-before-save command — see § The verify field | +| `setup_instructions` | recommended | Steps to configure the connector | +| `mcp_config` | ✅ | Configuration for the MCP client (`command`/`args` for `mcp_local`, `url`/`transport` for `mcp_remote`) | +| `homepage` | optional | Service URL | + +## Reserved enums + +### `type` (connector type) + +| Value | Description | Examples | +|-------|-------------|----------| +| `mcp_remote` | Hosted MCP server, reachable via URL | Tavily, Exa | +| `mcp_local` | Script to run locally | Gmail, Google Calendar, WhatsApp | +| `script` | Standalone script (non-MCP) | *(future)* | + +### `scope` (configuration scope) + +| Value | Description | Examples | +|-------|-------------|----------| +| `global` | A single instance/config for the whole system | Tavily, Weather, Google Trends | +| `user` | Each user has their own instance/authentication | Gmail, WhatsApp, Google Calendar | + +### `requires` (prerequisites) + +| Value | Description | +|-------|-------------| +| `API_KEY` | Requires an API key to configure | +| `OAUTH` | Requires OAuth authentication (Google, etc.) | +| `DOCKER` | Requires Docker Engine | +| `NODE` | Requires Node.js runtime | +| `PYTHON` | Requires Python 3 | +| `ENV` | Requires environment variables (declared in the manifest `env` field) | +| `SECRETS_DIR` | ❌ **Deprecated** — the `secrets/` folder is removed from the model; connectors must use `ENV`/`SECRET` (see § Placeholder syntax) | + +## The `auth` field + +Structure describing how the connector handles authentication: + +```json +// API key in query string +{"type": "api_key", "delivery": "query", "param": "tavilyApiKey"} + +// API key in header +{"type": "api_key", "delivery": "header", "param": "X-API-Key"} + +// API key delivered as an environment variable +{"type": "api_key", "delivery": "env", "param": "GOOGLE_MAPS_API_KEY"} + +// OAuth2 — provider is ONLY a slug (Skald resolves endpoints + client secrets) +{"type": "oauth2", "provider": "google", "scopes": ["…", "…"]} + +// OAuth2 with deliver (Skald injects the authorized_user JSON via env var) +{"type": "oauth2", "provider": "google", "scopes": ["…"], + "deliver": {"as": "env", "format": "google_authorized_user", "env": "GMAIL_CREDS_JSON"}} + +// OAuth2 with file-based deliver (legacy) +{"type": "oauth2", "provider": "google", "scopes": ["…"], + "deliver": {"as": "file", "format": "google_authorized_user", "path": "{secrets}/gmail_creds.json"}} + +// Password / app-password provided via environment variables +{"type": "password", "delivery": "env"} + +// QR-code pairing at runtime (WhatsApp) +{"type": "qr"} + +// No authentication +{"type": "none"} +``` + +### The `deliver` field (OAuth2 only) + +Declares **how** Skald delivers the obtained OAuth credential to the MCP server process. + +| Field | Required | Description | +|-------|----------|-------------| +| `as` | ✅ | `"file"` (on disk) or `"env"` (environment variable) | +| `format` | ✅ | Name of the serialization — e.g. `"google_authorized_user"` (Google JSON that `from_authorized_user_file` reads), `"refresh_token"`, `"access_token"` | +| `path` | `as=file` only | Path with the `{secrets}` placeholder (Skald expands it to a per-user dir at runtime). MUST match the path in `mcp_config.env`. | +| `env` | `as=env` only | Name of the environment variable into which Skald injects the entire authorized_user JSON. **Must not be declared in `mcp_config.env`** — Skald injects it at runtime. | + +The feed NEVER contains `client_id`, `client_secret`, the endpoint URL, or `redirect_uri`. These are +resolved on the Skald side from the `provider` name. + +## The `env` field (environment variables) + +An **array** declaring the environment variables the user must provide to make the connector work. +**No credential lands on disk nor in `secrets/`** — the host collects the values via a real form +(masking `secret: true` fields, enforcing `required`, using `example` as placeholder) and injects them +into the MCP server process environment at launch. The server reads them from `os.environ`. + +```json +"env": [ + { + "name": "EMAIL_IMAP_HOST", + "label": "IMAP host", + "description": "IMAP server hostname (e.g. imap.gmail.com)", + "required": true, + "secret": false, + "example": "imap.gmail.com" + }, + { + "name": "EMAIL_PASSWORD", + "label": "Password / app password", + "description": "Password or app-password of the provider", + "required": true, + "secret": true, + "default": "" + } +] +``` + +| Field | Required | Description | +|-------|----------|-------------| +| `name` | ✅ | Name of the environment variable (UPPER_SNAKE_CASE) | +| `label` | ✅ | Short label for the UI | +| `description` | ✅ | Help text | +| `required` | ✅ | If `true`, the host forces the user to provide a value | +| `secret` | recommended | If `true`, sensitive value (masked, not logged) | +| `default` | optional | Value used when not provided (non-required fields only) | +| `example` | optional | Example placeholder for the UI | + +The `email` connector is the reference example. `tavily`, `gmaps`, and `linkedin` also declare `env[]`. + +## Placeholder syntax (unified) + +Every value skald must fill at runtime with user-provided data uses **one of two tokens**, wherever it +appears (URL, `mcp_config.env`, `verify.command`): + +| Token | Meaning | Example | +|-------|---------|---------| +| `{ENV:NAME}` | Non-sensitive variable (hostname, port, username…) | `{ENV:EMAIL_IMAP_HOST}` | +| `{SECRET:NAME}` | Sensitive variable (password, API key, token) | `{SECRET:EMAIL_PASSWORD}` | + +`NAME` is the `name` field declared in the `env[]` array. skald collects the values via a form (masking +`{SECRET:}` fields), injects them as environment into the MCP server / verify process, and substitutes +the tokens in the manifest. + +Rules: +- Unrecognized tokens (`{secrets}/…`, legacy `{key}`, `{env:NAME}`) are **deprecated**: skald does not + substitute them and the manifest must be updated. +- `{SECRET:}` is reserved for the primary key when `auth.type = "api_key"` (e.g. Tavily: + `?tavilyApiKey={SECRET:tavilyApiKey}`). skald also treats that value as the API key for bearer/header + routing. +- A `{ENV:X}` or `{SECRET:X}` token whose `X` is not in the manifest's `env[]` is substituted with an + empty string (the host cannot guess it). + +### Deprecations + +| Token | Status | Replacement | +|-------|--------|-------------| +| `{key}` | ❌ deprecated | `{SECRET:}` | +| `{env:NAME}` | ❌ deprecated | `{ENV:NAME}` | +| `{secrets}/…` | ❌ deprecated | Declare the path as `{ENV:…}` (the `secrets/` folder is removed from the model) | + +## The `verify` field (test before save) + +Declares a shell command that skald runs **after** the user fills in the form and **before** persisting +the activation, to confirm the credentials just entered actually work. + +```json +"verify": { + "command": "python3 verify.py", + "timeout_secs": 20 +} +``` + +| Field | Required | Description | +|-------|----------|-------------| +| `command` | ✅ | Shell command. Runs in the same sandbox as the server: container `skald-{userid}` for `mcp_local` user, host for `mcp_remote` global. The declared env/secrets are injected | +| `timeout_secs` | optional | Default 15. skald kills the process at expiry | + +### Output convention + +The command must print **a single JSON object on stdout** and nothing else: + +```json +{"ok": true, "message": "IMAP and SMTP authentication successful", "details": {"imap": "…", "smtp": "…"}} +{"ok": false, "message": "IMAP login failed: INVALID_CREDENTIALS"} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `ok` | bool | `true` = test passed | +| `message` | string | Message shown to the user (never log secrets inside) | +| `details` | object | Optional, structured details shown in `
` |
+
+Exit code: 0 on success, ≠ 0 on failure (skald uses the exit code as a fallback if the JSON parse
+fails). **Never print credentials** in `message`/`details`.
+
+### Where to put the script
+
+If `command` references a file (e.g. `verify.py`), the file must be saved in the connector folder
+(`/verify.py`); `compile.py` then picks it up into `files[]` with its SHA-256 and size. skald
+downloads it, verifies the hash against the index, and makes it available at the same path as the main
+server (container for `mcp_local`, `./scripts//` on the host for `mcp_remote`). A `verify` command
+may also be fully inline (see `firecrawl`, which uses a `node -e "…"` one-liner).
+
+### Without `verify`
+
+If `verify` is absent, skald **runs no test** — activation is direct and the connector goes to
+`auth_state='ready'` without verification. For `mcp_remote` there is no handshake fallback: the manifest
+author decides whether the test is needed by writing `verify`.
+
+## Friendly tool names
+
+Every MCP tool must expose a friendly name for the Skald UI. Two ways, in order of preference:
+
+1. **Via the MCP script (preferred)** — add `"title": "Friendly Name"` to each tool definition returned by
+   `tools/list`. Works for all local scripts (Python/Node) that we control.
+2. **Via the manifest (fallback)** — add `"tools": [{"name": "…", "display_name": "…"}]` in
+   `fragment.json` **and** in `connector.json`. Used only for remote connectors or external packages
+   (e.g. `npx -y firecrawl-mcp`).
+
+**Resolution order** used by Skald:
+1. `tools[].display_name` from the manifest
+2. `title` from the MCP server's `tools/list`
+3. Automatic prettify of the raw name (`send_message` → "Send Message")
+
+As of 2026-07-21 all marketplace connectors carry `title` in the script or `tools[]` in the manifest.
+
+## MCP server conventions
+
+The local MCP servers (`connectors/gmail/gmail_mcp_server.py`, `connectors/email/email_mcp_server.py`,
+`connectors/wikipedia/`, `connectors/weather/`, `connectors/gmaps/`, …) are hand-rolled JSON-RPC 2.0
+servers over stdio — no MCP SDK / FastMCP. Shared conventions, which new local connectors must mirror:
+
+- **stdout is reserved for JSON-RPC**; all logging goes to stderr, and a lock guards stdout writes.
+- `handle_request` implements the full handshake: `initialize` → `protocolVersion 2024-11-05` +
+  `serverInfo`, a silent `notifications/initialized`, `ping` → `{}`, and `tools/list` → `{"tools": TOOLS}`
+  (an **object**, not a bare array).
+- **Notifications are never answered**: any message without an `id` returns `None`.
+- Tools are declared as a `TOOLS` manifest list plus a `TOOL_DISPATCH` map.
+- Where relevant, a background thread emits push notifications (e.g. `event/new_email`).
+
+Connector-specific notes:
+
+- **Gmail** — Gmail API with OAuth. Skald delivers the credential as `deliver: env/google_authorized_user`
+  into `GMAIL_CREDS_JSON`; the script also falls back to `GMAIL_CREDS_PATH` or `./secrets/gmail_creds.json`
+  for standalone use (that `secrets/` path is **deprecated** and gitignored). Push = History API polling.
+  `verify` is not yet wired — it awaits Phase 2 (OAuth via loopback listener).
+- **Email** — generic IMAP+SMTP, **stdlib-only (no dependencies)**, configured entirely from `{ENV:}`/
+  `{SECRET:}` env vars, works with any provider. Push = IMAP IDLE with a 60s polling fallback; the request
+  thread and the watcher thread each hold their own IMAP connection (imaplib is not thread-safe). Note:
+  imaplib does not quote SEARCH arguments, so values with spaces must be wrapped via `_q()`.
+- **Tavily** — `mcp_remote`/`global`; API key declared as an `env[]` secret and templated into the URL as
+  `{SECRET:tavilyApiKey}`.
+- **SSH** — `mcp_local`/`user`; stores aliases in `~/.ssh_aliases.json` (auto-managed, 0600). Auth
+  per-alias: key/agent (default) or elicited password. Sudo via `nopasswd` or elicited password
+  (`sudo -S`). No setup-time credentials: `auth.type: "none"` with no `verify`. All `SSH_MCP_*` env vars
+  are optional with defaults.
+
+## Current connectors
+
+Order as in `connectors/index.json` (18 connectors).
+
+| ID | Name | Type | Scope | Auth | Verify |
+|----|------|------|-------|------|--------|
+| `gmail` | Gmail | `mcp_local` | `user` | oauth2 (Google) + deliver `env/google_authorized_user` (`GMAIL_CREDS_JSON`) | ⏳ Phase 2 — OAuth via loopback listener |
+| `gcal` | Google Calendar | `mcp_local` | `user` | oauth2 (Google) + deliver `env/…` (`GCAL_CREDS_JSON`) | `verify.py` (creds load + API probe) |
+| `drive` | Google Drive | `mcp_local` | `user` | oauth2 (Google) + deliver `env/…` (`DRIVE_CREDS_JSON`) | `verify.py` (creds load + Drive API probe) |
+| `email` | Email (IMAP/SMTP) | `mcp_local` | `user` | password (env) | `verify.py` (IMAP+SMTP probe) |
+| `exa` | Exa | `mcp_remote` | `global` | api_key (`{SECRET:exaApiKey}` in URL — optional, free tier) | `verify.py` (MCP initialize probe) |
+| `firecrawl` | Firecrawl | `mcp_local` | `global` | api_key (env) | inline `node -e` scrape probe |
+| `http-fetch` | HTTP Fetch | `mcp_local` | `global` | none | — |
+| `serpapi-flights` | SerpAPI Flights | `mcp_remote` | `global` | api_key (`{SECRET:serpapiApiKey}` in URL) | `verify.py` (MCP initialize probe) |
+| `ssh` | SSH Remote Access | `mcp_local` | `user` | none (auth at runtime, per-alias) | — |
+| `tavily` | Tavily | `mcp_remote` | `global` | api_key (`{SECRET:tavilyApiKey}` in URL) | `verify.py` (HTTP probe `/search`) |
+| `weather` | Weather (Open-Meteo) | `mcp_local` | `global` | none | — |
+| `whatsapp` | WhatsApp | `mcp_local` | `user` | qr | — |
+| `wikipedia` | Wikipedia | `mcp_local` | `global` | none | — |
+| `context7` | Context7 | `mcp_remote` | `global` | none | `verify.py` (MCP initialize probe) |
+| `gmaps` | Google Maps | `mcp_local` | `global` | api_key (env: `GOOGLE_MAPS_API_KEY`) | `verify.py` (Geocoding API probe) |
+| `google-trends` | Google Trends | `mcp_local` | `global` | none | `verify.py` (trendspyg import probe) |
+| `linkedin` | LinkedIn | `mcp_local` | `user` | api_key (env: `LINKEDIN_LI_AT` session cookie) | `verify.py` |
+| `playwright` | Playwright | `mcp_local` | `global` | none | `verify.js` (headless Chromium launch probe) |
+
+A connector without `verify` is activated without any test — see § Without verify.
+
+## Icon conventions
+
+- **Format**: SVG for vector icons (better for retina/zoom), PNG for raster.
+- **Name**: `icon_sm.{svg|png}` (small, ~48×48px), `icon_lg.{svg|png}` (large, ~96×96px).
+- **Path**: relative to the connector folder in `connector.json`; `{folder}/{filename}` in the index
+  (e.g. `gmail/icon_sm.svg`).
+
+## File integrity (sha256)
+
+- SHA-256 hashes are generated **automatically** by `scripts/compile.py` from the physical files present
+  in each folder — never manual, never stale.
+- `compile.py` excludes from `files[]`: `fragment.json`, `connectors.json`, `index.json`, `compile.sh`,
+  `compile.py`, `update_hashes.py`, `.DS_Store`, and the `scripts/`, `__pycache__/`, `.git/` directories.
+  Everything else in the folder is hashed — **including `connector.json`**.
+- `fragment.json` and `index.json` have no hash: they are compilation inputs only.
+- The only file signed (in the future) will be `connectors.json`, the index itself.
 
 ## Critical invariants
 
-- **Editing any shipped connector file (script, icon, requirements, `verify.py`) requires updating its `sha256` AND `size` in `connectors.json`.** A stale hash breaks integrity verification on the client. `connector.json` and `connectors.json` themselves are not hashed, so editing them needs no hash update.
-- **Adding a connector** means changes in two places: a new entry in `connectors.json` (with all file hashes) *and* a new `connectors//connector.json`. Keep `id`, `scope`, `type`, `tags`, and `requires` consistent between the two.
-- **A new `verify.py` must be added to `files[]`** (with `sha256`+`size`) exactly like any other shipped file — skald refuses to run an unverified script.
-- The `secrets/` directory is **deprecated** in the model (credentials flow through `{ENV:}`/`{SECRET:}`). Gmail still uses it locally pending the OAuth migration; that path is gitignored — never commit credentials or OAuth tokens.
+- **Never edit `connectors.json` by hand.** It is generated. Edit `fragment.json` / `index.json` /
+  the connector files, then run `python3 scripts/compile.py`.
+- **Bump `version` (+1) and `version_string` on every file change** to a connector, in both
+  `fragment.json` and `connector.json`, and keep `version_release_date` current.
+- **Keep `id`, `name`, `type`, `scope`, `tags`, `requires`, and `auth` consistent** between
+  `fragment.json` and `connector.json`.
+- **Adding a `verify` script means shipping the file in the connector folder** and recompiling — skald
+  refuses to run a script whose SHA-256 is not pinned in the index.
+- **Always run `python3 scripts/compile.py` before deploying**; a stale hash breaks integrity
+  verification on the client.
+- The `secrets/` directory is **deprecated** in the model (credentials flow through `{ENV:}`/`{SECRET:}`).
+  Gmail still uses it locally pending the OAuth migration; that path is gitignored — never commit
+  credentials or OAuth tokens.
 
-## Changelog
+## Local workflow
 
-Every user-facing change to the marketplace (new connector, connector fix, version bump, icon update, deploy notes, etc.) must be recorded in **`CHANGELOG.md`** at the repo root. Rules:
+1. **Adding a new connector**:
+   - Create the folder `connectors//`
+   - Create `fragment.json` (id, name, type, scope, icons, auth, tools, …)
+   - Create `connector.json` (technical config: mcp_config, launch_command, env, verify, …)
+   - Add the MCP script, icons, `verify.py`, `requirements.txt`
+   - Add the id to `connectors/index.json`
+   - Run `python3 scripts/compile.py`
 
-- Follow the classic [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format (version headings, `Added`/`Changed`/`Fixed`/`Removed` categories, `[Unreleased]` section at the top). Use **date headings** (`## 2026-08-10`) instead of semver since each connector has its own version.
-- Write in English.
-- Add the entry in the same commit/change that modifies the connector files — do not defer it.
-- Never put changelog entries in `SKALD.md`; it is spec-only.
+2. **Modifying an existing connector**:
+   - Edit the files in the connector folder, bump `version` / `version_string` / `version_release_date`
+   - **Do not touch** `connectors.json` — it gets regenerated
+   - Run `python3 scripts/compile.py`
+
+3. **Before deploying**:
+   ```bash
+   python3 scripts/compile.py           # regenerates connectors.json with fresh SHA-256s
+   python3 scripts/compile.py --verify  # (optional) checks the index is up to date
+   ```
 
 ## Commands
 
-Recompute a file hash (do this before deploy for every changed file; SKALD.md mentions a `scripts/update_hashes.py` helper that does not exist in the repo yet):
-```bash
-python3 -c "import hashlib; print(hashlib.sha256(open('connectors/gmail/gmail_mcp_server.py','rb').read()).hexdigest())"
-```
-
 Preview the catalog locally (must serve from the `connectors/` root so `/connectors.json` resolves):
 ```bash
 cd connectors && python3 -m http.server 8000   # then open http://localhost:8000/
 ```
 
-Set up and run the Gmail connector:
+Recompute a single file hash by hand (normally unnecessary — `compile.py` does it):
 ```bash
-pip install -r connectors/gmail/requirements.txt
-python3 connectors/gmail/gmail_oauth_setup.py    # opens browser for OAuth, writes token
-python3 connectors/gmail/gmail_mcp_server.py      # speaks JSON-RPC on stdin/stdout
+python3 -c "import hashlib; print(hashlib.sha256(open('connectors/gmail/gmail_mcp_server.py','rb').read()).hexdigest())"
 ```
 
-Deploy to the production server (Caddy, path `/var/www/connectors.skaldagent.net/`):
+Set up and run the Gmail connector standalone:
 ```bash
-rsync -avz --delete ./connectors/ dguiducci@skald-server:/var/www/connectors.skaldagent.net/
-# then fix ownership/permissions on the server:
-sudo chown -R caddy:caddy /var/www/connectors.skaldagent.net/
-sudo find /var/www/connectors.skaldagent.net/ -type f -exec chmod 644 {} \;
+pip install -r connectors/gmail/requirements.txt
+python3 connectors/gmail/gmail_mcp_server.py    # speaks JSON-RPC on stdin/stdout
 ```
-Remote git: `https://git.skaldagent.net/dguiducci/skald-connectors.git` (branch `main`). Live site: `https://connectors.skaldagent.net/`.
+
+## Deploy
+
+The remote server:
+
+- **Host**: skald-home-server (192.168.1.100 / 145.40.169.107)
+- **User**: dguiducci
+- **Path**: `/var/www/connectors.skaldagent.net/` (served by Caddy)
+- **Owner**: `caddy:caddy` — sudo required to write in `/var/www/`
+
+Deploy runs [deploy.sh](deploy.sh) on the server (`git pull` in
+`/home/dguiducci/repos/skald-connectors/`, then `sudo cp -r connectors/* /var/www/connectors.skaldagent.net/`):
+
+```bash
+ssh dguiducci@skald-home-server /home/dguiducci/marketplace_deploy.sh
+```
+
+or via MCP SSH:
+
+```bash
+mcp__ssh__exec alias=skald-home-server command="/home/dguiducci/marketplace_deploy.sh"
+```
+
+Then verify on `https://connectors.skaldagent.net/`.
+
+**Remember to run `python3 scripts/compile.py` and commit before deploying** — the server pulls from git.
+
+## Changelog
+
+Every user-facing change to the marketplace (new connector, connector fix, version bump, icon update,
+deploy notes, etc.) must be recorded in **`CHANGELOG.md`** at the repo root. Rules:
+
+- Follow the classic [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format (version headings,
+  `Added`/`Changed`/`Fixed`/`Removed` categories, `[Unreleased]` section at the top). Use **date headings**
+  (`## 2026-08-10`) instead of semver, since each connector has its own version.
+- Write in English.
+- Add the entry in the same commit/change that modifies the connector files — do not defer it.
diff --git a/SKALD.md b/SKALD.md
deleted file mode 100644
index 43b0eed..0000000
--- a/SKALD.md
+++ /dev/null
@@ -1,543 +0,0 @@
-# Skald Connectors Marketplace
-
-## What it is
-
-The **marketplace** is the catalog of tested connectors for Skald. Each connector is an adapter that lets Skald interface with an external service (API, email, calendar, search, messaging, etc.).
-
-Two types of connectors:
-
-- **`mcp_remote`** — an already-hosted MCP server, reachable via URL (e.g. Tavily).
-- **`mcp_local`** — a Python/Node script to run client-side (e.g. Gmail, Google Calendar).
-
-## Local references
-
-- **[docs/connector.manifest_guide.md](docs/connector.manifest_guide.md)** — Official guide for producing correct connectors (copied from `skald-circle/blueprint/`)
-
-**Remote**: `https://git.skaldagent.net/dguiducci/skald-connectors.git`
-**Live**: `https://connectors.skaldagent.net/`
-**OAuth callback**: `https://connectors.skaldagent.net/oauth/show.html`
-
-## Deploy
-
-The **`main`** branch is the **release** branch. Only production-ready code ends up here.
-Development and alpha versions will live on separate branches in the future.
-
-Quick deploy with MCP SSH:
-
-```bash
-mcp__ssh__exec alias=skald-home-server command="/home/dguiducci/marketplace_deploy.sh"
-```
-
-Or via classic SSH:
-
-```bash
-ssh dguiducci@skald-home-server /home/dguiducci/marketplace_deploy.sh
-```
-
-The script on the server does:
-1. `git pull` in `/home/dguiducci/repos/skald-connectors/`
-2. `cp -r connectors/*` into `/var/www/connectors.skaldagent.net/`
-
-The `/var/www/connectors.skaldagent.net/` directory is owned by `dguiducci`,
-so no sudo is needed for the copy.
-
-**Before deploying**, remember to regenerate the index:
-```bash
-python3 scripts/compile.py
-```
-
-## Friendly tool names (2026-07-21)
-
-Every MCP tool must expose a friendly name for the Skald UI. Two ways, in order of preference:
-
-1. **Via the MCP script (preferred)** — add `"title": "Friendly Name"` in the definition of each tool inside `tools/list`. Works for all local scripts (Python/Node) that we control.
-2. **Via the manifest (fallback)** — add `"tools": [{"name": "...", "display_name": "..."}]` in `connector.json` **and** in `connectors.json`. Used only for remote connectors or external packages (e.g. `npx -y firecrawl-mcp`).
-
-**Resolution order** (Skald tries them in this order):
-1. `tools[].display_name` from the manifest
-2. `title` from the MCP server's `tools/list`
-3. Automatic prettify of the raw name (`send_message` → "Send Message")
-
-Current status (2026-07-21): all 13 marketplace connectors have `title` in the script or `tools[]` in the manifest.
-
-## Directory structure
-
-```
-connectors/
-├── index.json              ← ordered list of connector ids (input for compile.py)
-├── connectors.json         ← COMPILED INDEX (generated by compile.py, do not edit)
-├── compile.py              ← generates connectors.json (launched by scripts/compile.py)
-├── index.html              ← Catalog UI (reads connectors.json via fetch)
-├── oauth/
-│   └── show.html           ← OAuth callback receiver
-├── gmail/                   ← one connector per folder
-│   ├── fragment.json        ← index fragment (id, name, type, ..., WITHOUT files[])
-│   ├── connector.json       ← technical configuration (mcp_config, auth.deliver, ...)
-│   ├── gmail_mcp_server.py  ← MCP script
-│   ├── gmail_oauth_setup.py ← OAuth setup script
-│   ├── requirements.txt     ← Python dependencies
-│   ├── icon_sm.svg          ← small icon (48×48)
-│   └── icon_lg.svg          ← large icon (e.g. 96×96)
-├── email/
-│   ├── fragment.json
-│   ├── connector.json
-│   ├── email_mcp_server.py
-│   ├── verify.py
-│   ├── requirements.txt
-│   ├── icon_sm.svg
-│   └── icon_lg.svg
-└── ...
-```
-
-## Schema — connectors.json (root)
-
-This is the **single root of trust**. It contains `type`, `scope`, and the sha256 of each connector's files.
-It has no hash of itself — in the future it may be digitally signed.
-
-```json
-{
-  "version": 1,
-  "connectors": [
-    {
-      "id": "gmail",
-      "name": "Gmail",
-      "type": "mcp_local",
-      "scope": "user",
-      "icon_small": "gmail/icon_sm.svg",
-      "icon_large": "gmail/icon_lg.svg",
-      "user_description": "Read, send, and manage Gmail emails via OAuth...",
-      "requires": ["OAUTH", "PYTHON"],
-      "tags": ["email", "mcp", "local", "google"],
-      "auth": {
-        "type": "oauth2",
-        "provider": "google",
-        "scopes": [
-          "https://www.googleapis.com/auth/gmail.modify",
-          "https://www.googleapis.com/auth/gmail.labels"
-        ]
-      },
-      "folder": "gmail",
-      "version": 1,
-      "version_string": "1.0.0",
-      "version_release_date": "2026-07-19",
-      "files": [
-        {"path": "gmail_mcp_server.py",  "sha256": "a50d4da9621f7a4b092f...", "size": 46772},
-        {"path": "gmail_oauth_setup.py", "sha256": "e488acb289c43a3e6d54...", "size": 3627},
-        {"path": "icon_lg.svg",          "sha256": "93c8d9c8dae96f0206e5...", "size": 254},
-        {"path": "icon_sm.svg",          "sha256": "029d7f5d81de6cf2b17b...", "size": 251},
-        {"path": "requirements.txt",     "sha256": "3f659cc5e5f0543f1326...", "size": 82}
-      ]
-    }
-  ]
-}
-```
-
-### Index fields
-
-| Field | Required | Description |
-|-------|----------|-------------|
-| `id` | ✅ | Unique identifier (kebab-case) |
-| `name` | ✅ | Displayed name |
-| `type` | ✅ | `mcp_remote` or `mcp_local` |
-| `scope` | ✅ | `global` or `user` |
-| `icon_small` | ✅ | Path relative to the marketplace root |
-| `icon_large` | ✅ | Path relative to the marketplace root |
-| `user_description` | ✅ | Short description for the UI |
-| `requires` | ✅ | Array of requirement enums |
-| `tags` | ✅ | Array of tags for filtering |
-| `folder` | ✅ | Name of the connector folder |
-| `version` | ✅ | Per-connector integer, +1 on every file change |
-| `version_string` | ✅ | Semver (display only) |
-| `version_release_date` | ✅ | ISO 8601 date YYYY-MM-DD (display only) |
-| `files` | ✅ | Array of files with sha256 (NO self-hash) |
-
-## Schema — connector.json (per folder)
-
-Technical configuration for connector activation.
-
-```json
-{
-  "id": "gmail",
-  "name": "Gmail",
-  "version": 1,
-  "version_string": "1.0.0",
-  "version_release_date": "2026-07-19",
-  "type": "mcp_local",
-  "scope": "user",
-  "launch_command": "python3 gmail_mcp_server.py",
-  "transport": "stdio",
-  "requires": ["OAUTH", "PYTHON"],
-  "tags": ["email", "mcp", "local", "google"],
-  "dependencies": [
-    "google-api-python-client>=2.150.0",
-    "google-auth>=2.35.0",
-    "google-auth-oauthlib>=1.2.0"
-  ],
-  "setup_instructions": [
-    "Install dependencies: pip install -r requirements.txt",
-    "Run: python3 gmail_oauth_setup.py (optional, for standalone use — Skald handles OAuth)"
-  ],
-  "docs": [
-    {
-      "lang": "en",
-      "description": "Full description for human users...",
-      "llm_short_description": "Google Calendar — read, create, update, delete, and respond to calendar events. Supports push notifications."
-    }
-  ],
-  "auth": {
-    "type": "oauth2",
-    "provider": "google",
-    "scopes": [
-      "https://www.googleapis.com/auth/gmail.modify",
-      "https://www.googleapis.com/auth/gmail.labels"
-    ],
-    "deliver": {
-      "as": "env",
-      "format": "google_authorized_user",
-      "env": "GMAIL_CREDS_JSON"
-    }
-  },
-  "mcp_config": {
-    "command": "python3",
-    "args": ["gmail_mcp_server.py"]
-  },
-  "homepage": "https://mail.google.com",
-  "icon_small": "icon_sm.svg",
-  "icon_large": "icon_lg.svg"
-}
-```
-
-### connector.json fields
-
-| Field | Required | Description |
-|-------|----------|-------------|
-| `id` | ✅ | Unique identifier (matches the folder name) |
-| `name` | ✅ | Displayed name |
-| `version` | ✅ | Per-connector integer, +1 on every file change |
-| `version_string` | ✅ | Semver (display only) |
-| `version_release_date` | ✅ | ISO 8601 date YYYY-MM-DD (display only) |
-| `type` | ✅ | `mcp_remote` or `mcp_local` |
-| `scope` | ✅ | `global` or `user` |
-| `requires` | ✅ | Array of requirement enums |
-| `tags` | ✅ | Array of tags |
-| `auth` | ✅ | Authentication configuration object |
-| `docs` | ✅ | Array of multilingual documentation. **`llm_short_description`** is the field that ends up in the LLM's system prompt — it must describe WHAT the connector DOES, not list its tools (the LLM sees them after `activate_tools`). Example: *"Weather — current conditions, 16-day forecast, and AQI data for any location."* |
-| `icon_small` | ✅ | Icon filename in the local folder |
-| `icon_large` | ✅ | Icon filename in the local folder |
-| `launch_command` | `mcp_local` only | Command to start the MCP server |
-| `transport` | `mcp_local` only | `stdio` (default) |
-| `dependencies` | recommended | Python/Node dependencies (empty array if stdlib only) |
-| `env` | if `requires` includes `ENV` | Environment variables the user must provide (schema for the UI) — see § The env field |
-| `setup_instructions` | recommended | Steps to configure the connector |
-| `mcp_config` | `mcp_local` only | Configuration for the MCP client |
-| `homepage` | optional | Service URL |
-
-## Reserved enums
-
-### type (connector type)
-
-| Value | Description | Examples |
-|-------|-------------|----------|
-| `mcp_remote` | Hosted MCP server, reachable via URL | Tavily, Weather |
-| `mcp_local` | Script to run locally | Gmail, Google Calendar, WhatsApp |
-| `script` | Standalone script (non-MCP) | *(future)* |
-
-### scope (configuration scope)
-
-| Value | Description | Examples |
-|-------|-------------|----------|
-| `global` | A single instance/config for the whole system | Tavily, Weather, Google Trends |
-| `user` | Each user has their own instance/authentication | Gmail, WhatsApp, Google Calendar |
-
-### requires (prerequisites)
-
-| Value | Description |
-|-------|-------------|
-| `API_KEY` | Requires an API key to configure |
-| `OAUTH` | Requires OAuth authentication (Google, etc.) |
-| `DOCKER` | Requires Docker Engine |
-| `NODE` | Requires Node.js runtime |
-| `PYTHON` | Requires Python 3 |
-| `SECRETS_DIR` | ❌ **Deprecated** — the `secrets/` folder is removed from the model; connectors must use `ENV`/`SECRET` (see § Placeholder syntax) |
-| `ENV` | Requires environment variables (declared in the manifest `env` field) |
-
-## The auth field
-
-Structure describing how the connector handles authentication:
-
-```json
-// API key in query string
-{"type": "api_key", "delivery": "query",  "param": "tavilyApiKey"}
-
-// API key in header
-{"type": "api_key", "delivery": "header", "param": "X-API-Key"}
-
-// OAuth2 — provider is ONLY a slug (Skald resolves endpoints + client secrets)
-{"type": "oauth2", "provider": "google", "scopes": ["...", "..."]}
-
-// OAuth2 with deliver (Skald injects the authorized_user JSON via env var)
-{"type": "oauth2", "provider": "google", "scopes": ["..."],
- "deliver": {"as": "env", "format": "google_authorized_user", "env": "GMAIL_CREDS_JSON"}}
-
-// OAuth2 with file-based deliver (legacy)
-{"type": "oauth2", "provider": "google", "scopes": ["..."],
- "deliver": {"as": "file", "format": "google_authorized_user", "path": "{secrets}/gmail_creds.json"}}
-
-// Password / app-password provided via environment variables
-{"type": "password", "delivery": "env"}
-
-// No authentication
-{"type": "none"}
-```
-
-### The deliver field (OAuth2 only)
-
-Declares **how** Skald delivers the obtained OAuth credential to the MCP server process.
-
-| Field | Required | Description |
-|-------|----------|-------------|
-| `as` | ✅ | `"file"` (on disk) or `"env"` (environment variable) |
-| `format` | ✅ | Name of the serialization — e.g. `"google_authorized_user"` (Google JSON that `from_authorized_user_file` reads), `"refresh_token"`, `"access_token"` |
-| `path` | `as=file` only | Path with the `{secrets}` placeholder (Skald expands it to a per-user dir at runtime). MUST match the path in `mcp_config.env`. |
-| `env` | `as=env` only | Name of the environment variable into which Skald injects the entire authorized_user JSON. **Must not be declared in `mcp_config.env`** — Skald injects it at runtime. |
-
-The feed NEVER contains: `client_id`, `client_secret`, `endpoint` URL, `redirect_uri`. These are resolved on the Skald side from the `provider` name.
-
-## The env field (environment variables)
-
-Used when `requires` includes `ENV`. It is an **array** declaring the environment
-variables the user must provide to make the connector work; **no credential
-lands on disk nor in `secrets/`** — the host collects the values, enforces
-filling in the required fields, and injects them as environment into the MCP
-server process at launch. The server reads them from `os.environ`.
-
-```json
-"env": [
-  {
-    "name": "EMAIL_IMAP_HOST",   // name of the environment variable
-    "label": "IMAP host",         // label for the UI
-    "description": "IMAP server hostname (e.g. imap.gmail.com)",
-    "required": true,             // if true, the host must enforce this field
-    "secret": false,              // if true, the UI masks it and treats it as a secret
-    "example": "imap.gmail.com"   // placeholder/example (optional)
-  },
-  {
-    "name": "EMAIL_PASSWORD",
-    "label": "Password / app password",
-    "description": "Password or app-password of the provider",
-    "required": true,
-    "secret": true,
-    "default": ""                 // default value if not required (optional)
-  }
-]
-```
-
-| Field | Required | Description |
-|-------|----------|-------------|
-| `name` | ✅ | Name of the environment variable (UPPER_SNAKE_CASE) |
-| `label` | ✅ | Short label for the UI |
-| `description` | ✅ | Help text |
-| `required` | ✅ | If `true`, the host forces the user to provide a value |
-| `secret` | recommended | If `true`, sensitive value (masked, not logged) |
-| `default` | optional | Value used when not provided (non-required fields only) |
-| `example` | optional | Example placeholder for the UI |
-
-## Placeholder syntax (unified)
-
-Every value skald must fill at runtime with user-provided data uses **one of
-two tokens**, wherever it appears (URL, `mcp_config.env`, `verify.command`):
-
-| Token | Meaning | Example |
-|-------|---------|---------|
-| `{ENV:NAME}` | Non-sensitive variable (hostname, port, username…) | `{ENV:EMAIL_IMAP_HOST}` |
-| `{SECRET:NAME}` | Sensitive variable (password, API key, token) | `{SECRET:EMAIL_PASSWORD}` |
-
-`NAME` is the name declared in the `env[]` array (the `name` field). skald
-collects the values via a form (masking `{SECRET:}` fields), injects them as
-environment into the MCP server / verify process, and substitutes the tokens
-in the manifest.
-
-Rules:
-- Unrecognized tokens (e.g. `{secrets}/…`, legacy `{key}`, `{env:NAME}`) are
-  **deprecated**: skald does not substitute them and the manifest must be updated.
-- `{SECRET:}` is reserved for the primary key when
-  `auth.type = "api_key"` (e.g. Tavily: `{SECRET:tavilyApiKey}`). skald also
-  treats that value as the API key for bearer/header routing.
-- A `{ENV:X}` or `{SECRET:X}` token whose `X` is not in the manifest's `env[]`
-  is substituted with an empty string (the host cannot guess it).
-
-### Deprecations
-
-| Token | Status | Replacement |
-|-------|--------|-------------|
-| `{key}` | ❌ deprecated | `{SECRET:}` |
-| `{env:NAME}` | ❌ deprecated | `{ENV:NAME}` |
-| `{secrets}/…` | ❌ deprecated | The connector must declare the path as `{ENV:…}` (the `secrets/` folder is removed from the model) |
-
-## The verify field (test before save)
-
-Declares a shell command that skald runs **after** the user has filled in the
-form and **before** persisting the activation. It checks that the credentials
-just entered actually work.
-
-```json
-"verify": {
-  "command": "python3 verify.py",
-  "timeout_secs": 20
-}
-```
-
-| Field | Required | Description |
-|-------|----------|-------------|
-| `command` | ✅ | Shell command. Runs in the same sandbox as the server: container `skald-{userid}` for `mcp_local` user, host for `mcp_remote` global. The declared env/secrets are injected |
-| `timeout_secs` | optional | Default 15. skald kills the process at expiry |
-
-### Output convention
-
-The command must print **a single JSON object on stdout** and nothing else:
-
-```json
-{"ok": true,  "message": "IMAP and SMTP authentication successful", "details": {"imap": "...", "smtp": "..."}}
-{"ok": false, "message": "IMAP login failed: INVALID_CREDENTIALS"}
-```
-
-| Field | Type | Description |
-|-------|------|-------------|
-| `ok` | bool | `true` = test passed |
-| `message` | string | Message shown to the user (never log secrets inside) |
-| `details` | object | Optional, structured details shown in `
` |
-
-Exit code: 0 on success, ≠ 0 on failure (skald uses the exit code as a
-fallback if the JSON parse fails). **Never print credentials** in the
-`message`/`details`.
-
-### Where to put the script
-
-If `command` references a file (e.g. `verify.py`), the file must:
-1. Be added to the `files[]` array in `connectors.json` (with `sha256` and `size`)
-2. Be saved in the connector folder (`/verify.py`)
-
-skald downloads it, verifies its SHA-256 against the index, and makes it
-available at the same path as the main server (container for `mcp_local` ones,
-`./scripts//` dir on the host for `mcp_remote` ones).
-
-### Without verify
-
-If `verify` is absent, skald **runs no test** — activation is direct, as
-today. The connector goes to `auth_state='ready'` without verification. For
-`mcp_remote` there is no handshake fallback: the manifest author decides
-whether the test is needed by writing `verify`.
-
-## Icon conventions
-
-- **Format**: SVG for vector icons (better for retina/zoom), PNG for raster
-- **Name**: `icon_sm.{svg|png}` (small, ~48×48px), `icon_lg.{svg|png}` (large, ~96×96px)
-- **Path**: relative to the connector folder
-- **In the index** the path is `{folder}/{filename}` (e.g. `gmail/icon_sm.svg`)
-
-## File integrity (sha256)
-
-- SHA-256 hashes are generated **automatically** by `scripts/compile.py` from
-  the physical files present in each folder — never manual, never stale.
-- The only file signed (in the future) will be `connectors.json` (the index).
-- `fragment.json` and `index.json` have no hash — they are just compilation inputs.
-
-## Local workflow
-
-1. **Adding a new connector**:
-   - Create the folder `connectors//`
-   - Create `fragment.json` (id, name, type, scope, icons, auth, tools, ...)
-   - Create `connector.json` (technical config: mcp_config, launch_command, ...)
-   - Add the MCP script, icons, verify.py, requirements.txt
-   - Add the id to `connectors/index.json`
-   - Run `python3 scripts/compile.py`
-
-2. **Modifying an existing connector**:
-   - Edit the files in the connector folder
-   - **Do not touch** `connectors.json` — it gets regenerated
-   - Run `python3 scripts/compile.py`
-
-3. **Before deploying**:
-   ```bash
-   python3 scripts/compile.py           # regenerates connectors.json with fresh SHA-256s
-   python3 scripts/compile.py --verify  # (optional) verifies that it is up to date
-   ```
-
-4. **Deploy to the server**:
-   ```bash
-   ssh dguiducci@skald-home-server /home/dguiducci/marketplace_deploy.sh
-   ```
-   or with MCP SSH:
-   ```bash
-   # via mcp__ssh__exec alias "skald-home-server"
-   /home/dguiducci/marketplace_deploy.sh
-   ```
-   (The deploy script does `git pull` + `cp -r connectors/* /var/www/connectors.skaldagent.net/`)
-
-5. Verify on `https://connectors.skaldagent.net/`
-
-## Fragment.json
-
-See [docs/connector.manifest_guide.md](docs/connector.manifest_guide.md) for the complete
-creation guide.
-
-`fragment.json` contains all the fields of a `connectors.json` entry **except** `files[]`.
-These are the required fields:
-
-| Field | Required | Description |
-|-------|----------|-------------|
-| `id` | ✅ | Unique identifier (matches the folder name) |
-| `name` | ✅ | Displayed name |
-| `type` | ✅ | `mcp_remote` or `mcp_local` |
-| `scope` | ✅ | `global` or `user` |
-| `icon_small` | ✅ | Path relative to the marketplace root |
-| `icon_large` | ✅ | Path relative to the marketplace root |
-| `user_description` | ✅ | Short description for the UI |
-| `requires` | ✅ | Array of requirement enums |
-| `tags` | ✅ | Array of tags for filtering |
-| `folder` | ✅ | Name of the connector folder (matches `id`) |
-| `version` | ✅ | Per-connector integer, +1 on every file change |
-| `version_string` | ✅ | Semver (display only) |
-| `version_release_date` | ✅ | ISO 8601 date (display only) |
-| `tools` | ✅ | Array of `{name, display_name}` for friendly UI names |
-| `auth` | optional | Authentication configuration (if other than `"none"`) |
-
-Note: the `files[]` array (with SHA-256 and size) is added **automatically**
-by `compile.py` by scanning the files present in the folder — it must never be
-written by hand.
-
-## Deploy to remote server
-
-The remote server is:
-
-- **Host**: skald-home-server (192.168.1.100 / 145.40.169.107)
-- **User**: dguiducci
-- **Path**: `/var/www/connectors.skaldagent.net/`
-- **Owner**: `caddy:caddy`
-- **Sudo**: required to write in `/var/www/`
-
-## Current connectors
-
-| ID | Name | Type | Scope | Auth | Verify |
-|----|------|------|-------|------|--------|
-| `context7` | Context7 | `mcp_remote` | `global` | none | `verify.py` (MCP initialize probe) |
-| `gmaps` | Google Maps | `mcp_local` | `global` | api_key (env: `GOOGLE_MAPS_API_KEY`) | `verify.py` (Geocoding API probe) |
-| `exa` | Exa | `mcp_remote` | `global` | api_key (`{SECRET:exaApiKey}` in URL — optional, free tier) | `verify.py` (MCP initialize probe) |
-| `tavily` | Tavily | `mcp_remote` | `global` | api_key (`{SECRET:tavilyApiKey}` in URL) | `verify.py` (HTTP probe `/search`) |
-| `serpapi-flights` | SerpAPI Flights | `mcp_remote` | `global` | api_key (`{SECRET:serpapiApiKey}` in URL) | `verify.py` (MCP initialize probe) |
-| `gmail` | Gmail | `mcp_local` | `user` | oauth2 (Google) + deliver: `env/google_authorized_user` (via `GMAIL_CREDS_JSON`) | ⏳ Phase 2 — OAuth via loopback listener |
-| `gcal` | Google Calendar | `mcp_local` | `user` | oauth2 (Google) + deliver: `env/google_authorized_user` (via `GCAL_CREDS_JSON`) | `verify.py` (creds load + API probe) |
-| `drive` | Google Drive | `mcp_local` | `user` | oauth2 (Google) + deliver: `env/google_authorized_user` (via `DRIVE_CREDS_JSON`) | `verify.py` (creds load + Drive API probe) |
-| `email` | Email (IMAP/SMTP) | `mcp_local` | `user` | password (env) | `verify.py` (IMAP+SMTP probe) |
-| `firecrawl` | Firecrawl | `mcp_local` | `global` | api_key (env) | — |
-| `http-fetch` | HTTP Fetch | `mcp_local` | `global` | none | — |
-| `ssh` | SSH Remote Access | `mcp_local` | `user` | none (auth runtime per-alias) | — |
-| `weather` | Weather (Open-Meteo) | `mcp_local` | `global` | none | — |
-| `google-trends` | Google Trends | `mcp_local` | `global` | none | `verify.py` (trendspyg import probe) |
-| `wikipedia` | Wikipedia | `mcp_local` | `global` | none | — |
-| `whatsapp` | WhatsApp | `mcp_local` | `user` | qr | — |
-| `playwright` | Playwright | `mcp_local` | `global` | none | `verify.js` (headless Chromium launch probe) |
-
-**verify-before-save status in skald**: `exa`, `drive`, `email`, `tavily`, and `gcal` have a complete `verify`
-(script + JSON output); `gmail` awaits Phase 2 (OAuth via loopback listener).
-A connector without `verify` is activated without any test — see § Without verify.