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

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

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

628 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/<id>/fragment.json`** — the connector's **index entry without `files[]`** (id, name, type,
scope, icons, `user_description`, requires, tags, version, `tools[]`, auth…).
- **`connectors/<id>/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
`/<folder>/`, 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:<auth.param>}` 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:<auth.param>}` |
| `{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 `<pre>` |
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
(`<id>/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/<id>/` 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/<id>/`
- 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.