# 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** — 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…). 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, plus a compile step: - **`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. ``` 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 └── … ``` ## 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

- **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.

## 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, env, verify, …)
   - 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, 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

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/
```

Recompute a single file hash by hand (normally unnecessary — `compile.py` does it):
```bash
python3 -c "import hashlib; print(hashlib.sha256(open('connectors/gmail/gmail_mcp_server.py','rb').read()).hexdigest())"
```

Set up and run the Gmail connector standalone:
```bash
pip install -r connectors/gmail/requirements.txt
python3 connectors/gmail/gmail_mcp_server.py    # speaks JSON-RPC on stdin/stdout
```

## 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.