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.
628 lines
29 KiB
Markdown
628 lines
29 KiB
Markdown
# 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.
|