add playwright connector (v1 / 1.0.0) + commit pending docs reorganization

- new global mcp_local connector wrapping @playwright/mcp@0.0.79:
  index.js argv wrapper (--headless --isolated --no-sandbox) importing
  the package cli.js, postinstall downloads chromium-only browser,
  verify.js headless-launch probe, 24 tools with display names
- also carries the pending docs work: changelog extracted to
  CHANGELOG.md, manifest guide, SKALD.md/CLAUDE.md updates
This commit is contained in:
Daniele
2026-08-19 17:37:13 +01:00
parent 28aeb9bcd7
commit fa8bbcb808
14 changed files with 1322 additions and 365 deletions
+261 -360
View File
@@ -1,64 +1,17 @@
# Skald Connectors Marketplace
## What it is
### 2026-08-10 — http-fetch + firecrawl: fix `npx` (v5 / 1.1.0)
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.).
Entrambi i connector erano **non funzionanti**: dichiaravano `mcp_config: {command: "npx", args: ["-y", "<package>"]}` e non spedivano nessun file di codice (solo `connector.json` + icone).
Two types of connectors:
- **`npx -y <package>` non è esprimibile in skald.** Per un `type: mcp_local` skald tratta `args[0]` come il **nome del file da eseguire**, non come un argomento: all'install calcola `script_path = "<id>/" + args[0]` e azzera `args_json` (`marketplace.rs::install`), poi `global_enable` (`api/mcp.rs`) lo risolve in path assoluto e lancia `<command> <abs>`. Il comando reale diventava `npx /…/connectors/http-fetch/-y` — un path inesistente, con `-y` e il nome del package spariti. Il processo non rispondeva mai a `initialize`, `start_server` falliva.
- **Il fallimento era silenzioso**: `global_enable` restituisce comunque HTTP 200 con un campo `error` nel body, quindi la UI mostrava il connector come abilitato mentre il runtime non aveva nessun server. E `render_mcp_list` (`loop_adapters/system.rs`) costruisce la tabella `## MCP servers` da `mcp.tools()`, cioè lo **stato vivo del runtime**, non dal DB → il connector risultava attivato e concesso all'utente ma **assente dal system context**. ⚠️ Questa combinazione (200 + `error` nel body) rende invisibile in UI qualsiasi connector che non parte: vale la pena farla emergere lato skald.
- **Fix — wrapper di due file** per entrambi: `package.json` che pinna il package upstream (`mcp-fetch-server@1.1.2`, `firecrawl-mcp@3.23.7`) e `index.js` che lo importa per side-effect (il modulo avvia il loop JSON-RPC su stdio all'import). `mcp_config` diventa `{command: "node", args: ["index.js"], transport: "stdio"}`, cioè un vero `local_script`: `ensure_installed_host` fa `npm ci --omit=dev || npm install --omit=dev` nella cartella del connector prima del lancio, esattamente come per whatsapp. Nessun `node_modules` spedito, nessun lockfile (come whatsapp).
- **Rimossi i campi legacy** `launch_command`, `transport` top-level e `dependencies` (`dependencies` è solo per la card, come già visto su gmaps; `transport` va dentro `mcp_config`).
- **firecrawl: rimosso `mcp_config.env: {"FIRECRAWL_API_KEY": "{SECRET:FIRECRAWL_API_KEY}"}`** — inerte, stesso caso di gmaps del 2026-08-10: `apply_key_placeholder` sostituisce i token solo nella URL, mai nei valori di `env`. Funziona perché la form admin manda `env` e quel payload sovrascrive `entry.env_json`.
- **firecrawl**: aggiunto `firecrawl_developer_search` a `tools[]` (27 tool live contro i 26 dichiarati, verificato su 3.23.7); `requires` `["NODE"]``["NODE", "API_KEY"]`.
- **Versioni riallineate** manifest↔fragment a `5` / `1.1.0` / `2026-08-10` per entrambi: erano 2/1.0.1 (manifest) vs 4/1.0.3 (fragment), e skald preferisce il manifest — quindi `installed_version` restava 2 e il badge "Update available" non sarebbe mai comparso.
- **Requisito host**: sono connector `scope: global`, girano sull'**host** e non nel container. `mcp-fetch-server` vuole Node ≥18, `firecrawl-mcp` vuole Node ≥**22**.
- Testati end-to-end riproducendo il path di skald (`npm ci || npm install` + `node <abs>/index.js`): `initialize`, `tools/list` e una `tools/call` reale, stdout solo JSON-RPC, stderr pulito ✅
- Indice rigenerato con compile.py ✅
- **`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).
⚠️ **Per applicarlo su un'istanza che ha già il connector installato non basta l'Update.** `refresh_connector_after_reinstall` (`skald/accessors.rs`) aggiorna della riga `mcp_global_servers` **solo la `description`**, poi riparte da quella riga: `command` e `args_json` restano quelli snapshottati al primo `global_enable`, cioè ancora `npx` + `/…/connectors/<id>/-y`. Procedura: deploy → **Update** dal marketplace (riscrive `script_path` nel catalogo) → aprire la pagina del connector e **ri-salvare la config**, che è l'unica chiamata che ricalcola `command`/`args` e lancia `ensure_installed_host`. Stessa cura già annotata per gmaps. Da valutare lato skald se il refresh debba ri-derivare anche command/args.
## Local references
### 2026-08-10 — gmaps: fix dipendenze + verify (v6 / 1.1.0)
- **Aggiunto `requirements.txt` (`googlemaps>=4.10.0`)** — era l'unico connector python senza. Le dipendenze erano dichiarate nel campo `dependencies` del manifest, che skald usa **solo per la card**: `ensure_installed_host` guarda esclusivamente `requirements.txt` / `package.json`. Risultato: `.pydeps` vuoto e log pieno di `No module named 'googlemaps'`, con il server che rispondeva comunque a `tools/list` (→ `connected — 6 tool(s)` su un connector non funzionante).
- **Cablato il `verify`** (`python3 verify.py`, 20s): `verify.py` era shippato ma il manifest non aveva il blocco `verify`, e skald legge `verify_command` solo da lì. Ora un'abilitazione con dipendenze rotte fallisce visibilmente invece di partire in silenzio.
- **`verify.py` si mette `.pydeps` su `sys.path`**: skald imposta `PYTHONPATH` solo per il processo *server* (`global_row_spec`), mentre il verify gira come `sh -c "python3 verify.py"` senza. Senza questa riga il verify fallirebbe con "Missing dependency" anche su un connector installato correttamente, disabilitando la riga. ⚠️ **Stesso rischio latente per ogni connector con `verify` che importa dipendenze non-stdlib** (gcal in container): da controllare.
- **Rimosso `mcp_config.env: {"GOOGLE_MAPS_API_KEY": "{SECRET:…}"}`** (introdotto il 2026-07-23): inerte. `apply_key_placeholder` sostituisce i token `{SECRET:}`/`{ENV:}` **solo nella URL**, mai nei valori di `env`. Funzionava perché la form admin manda `env` e quel payload sovrascrive `entry.env_json`; a form vuota il processo avrebbe ricevuto la stringa letterale. ⚠️ **La spec in questo file e in CLAUDE.md dice il contrario** — o si corregge la spec, o si estende la sostituzione ai valori di `env` lato skald.
- **Versioni riallineate** manifest↔fragment a `6` / `1.1.0` / `2026-08-10`: erano 2/1.0.1 vs 5/1.0.4, e skald preferisce il manifest (`manifest.version.or(entry.version)`) — quindi `installed_version` restava 2 e il badge "Update available" non sarebbe mai comparso.
- `requires`: `ENV``API_KEY`; messaggi d'errore del server ripuliti dai riferimenti a `secrets/gmaps_api_key.txt` (path deprecato).
- Indice rigenerato con compile.py ✅
### 2026-08-07 — Nuovo connector: LinkedIn
- Aggiunto connector `linkedin` (mcp_local, scope user): server.py + session.py + verify.py + icone PNG
- Aggiunto a `connectors/index.json`, indice rigenerato con compile.py (17 connector totali)
- `.gitignore` aggiornato per ignorare `.claude/`
- Deploy su connectors.skaldagent.net via alias `skaldserver` (192.168.1.100, LAN — niente Tailscale)
### 2026-07-23 — gmaps: fix env var injection
- Aggiunto `mcp_config.env` in `connector.json` per iniettare `GOOGLE_MAPS_API_KEY` nel processo MCP
- Il connector era dichiarato come `delivery: env` ma senza `mcp_config.env` Skald non sapeva passare la variabile al processo Python
- Version bump: fragment 4→5, connector 1→2
- Indice rigenerato con compile.py ✅
### 2026-07-23 — Context7 icon update (PNG)
- Sostituite icone Context7 da SVG a PNG (icona nuova fornita dall'utente):
- `icon_sm.png` — 48×48 (2.7 KB)
- `icon_lg.png` — 96×96 (4.5 KB)
- Vecchi SVG rimossi, riferimenti aggiornati in fragment.json e connector.json
- Version bump: fragment 3→4, connector 1→2
- Indice rigenerato con compile.py ✅
## Reference locale
### 2026-07-23 — SerpAPI Flights icon update (PNG)
- Sostituite icone SerpAPI Flights da SVG a PNG:
- `icon_sm.png` — 48×48 (2.9 KB)
- `icon_lg.png` — 96×96 (6.8 KB)
- Vecchi SVG rimossi, riferimenti aggiornati in fragment.json e connector.json
- Version bump: fragment 4→5, connector 2→3
- Indice rigenerato con compile.py ✅
- **[docs/connector.manifest_guide.md](docs/connector.manifest_guide.md)** — Guida ufficiale per generare connector corretti (copiata da `skald-circle/blueprint/`)
_Updated: 2026-07-23_
- **[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/`
@@ -66,75 +19,65 @@ _Updated: 2026-07-23_
## Deploy
Il branch **`main`** è il branch di **release**. Solo codice pronto per produzione finisce qui.
Sviluppo e versioni alfa-staranno su branch separati in futuro.
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.
Deploy rapido con MCP SSH:
Quick deploy with MCP SSH:
```bash
mcp__ssh__exec alias=skald-home-server command="/home/dguiducci/marketplace_deploy.sh"
```
Oppure via SSH classico:
Or via classic SSH:
```bash
ssh dguiducci@skald-home-server /home/dguiducci/marketplace_deploy.sh
```
Lo script sul server fa:
The script on the server does:
1. `git pull` in `/home/dguiducci/repos/skald-connectors/`
2. `cp -r connectors/*` in `/var/www/connectors.skaldagent.net/`
2. `cp -r connectors/*` into `/var/www/connectors.skaldagent.net/`
La directory `/var/www/connectors.skaldagent.net/` è di proprietà di `dguiducci`,
quindi non serve sudo per la copia.
The `/var/www/connectors.skaldagent.net/` directory is owned by `dguiducci`,
so no sudo is needed for the copy.
**Prima del deploy**, ricordarsi di rigenerare l'indice:
**Before deploying**, remember to regenerate the index:
```bash
python3 scripts/compile.py
```
## Cos'è
Il **marketplace** è il catalogo dei connector testati per Skald. Ogni connector è un adattatore che permette a Skald di interfacciarsi con un servizio esterno (API, email, calendario, ricerca, messaggistica, ecc.).
Due tipi di connector:
- **`mcp_remote`** — un MCP server già hosted, accessibile via URL (es. Tavily).
- **`mcp_local`** — script Python/Node da eseguire lato client (es. Gmail, Google Calendar).
## Friendly tool names (2026-07-21)
Ogni tool MCP deve esporre un nome friendly per la UI di Skald. Due modi, in ordine di preferenza:
Every MCP tool must expose a friendly name for the Skald UI. Two ways, in order of preference:
1. **Via script MCP (preferito)** — aggiungere `"title": "Friendly Name"` nella definizione di ogni tool dentro `tools/list`. Funziona per tutti gli script locali (Python/Node) che controlliamo.
2. **Via manifest (fallback)** — aggiungere `"tools": [{"name": "...", "display_name": "..."}]` in `connector.json` **e** in `connectors.json`. Usato solo per connector remoti o package esterni (es. `npx -y firecrawl-mcp`).
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`).
**Ordine di risoluzione** (Skald li prova in quest'ordine):
1. `tools[].display_name` dal manifest
2. `title` dal `tools/list` dell'MCP server
3. Prettify automatico del nome raw (`send_message` → "Send Message")
**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")
Stato attuale (2026-07-21): tutti i 13 connector del marketplace hanno `title` nello script o `tools[]` nel manifest.
Current status (2026-07-21): all 13 marketplace connectors have `title` in the script or `tools[]` in the manifest.
## Struttura directory
## Directory structure
```
connectors/
├── index.json ← lista ordinata degli id dei connector (input per compile.py)
├── connectors.json ← INDICE COMPILATO (generato da compile.py, non editare)
├── compile.py ← genera connectors.json
├── index.html ← Catalogo UI (legge connectors.json via fetch)
├── 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 (client-side, no backend)
├── gmail/ ← Un connector per cartella
│ ├── fragment.json ← Frammento dell'indice (id, name, type, ..., SENZA files[])
│ ├── connector.json ← Configurazione tecnica
│ ├── gmail_mcp_server.py ← Script MCP
│ ├── gmail_oauth_setup.py ← Script setup OAuth
│ ├── requirements.txt ← Dipendenze Python
│ ├── icon_sm.svg ← Icona piccola (48×48)
│ └── icon_lg.svg ← Icona grande (es. 96×96)
│ └── 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
@@ -143,27 +86,13 @@ connectors/
│ ├── requirements.txt
│ ├── icon_sm.svg
│ └── icon_lg.svg
── ssh/
│ ├── fragment.json
│ ├── connector.json
│ ├── ssh_mcp_server.py
│ ├── requirements.txt
│ ├── icon_sm.svg
│ └── icon_lg.svg
└── tavily/
├── fragment.json
├── connector.json
├── verify.py
├── icon_sm.png
└── icon_lg.png
```
└── icon_lg.png
── ...
```
## Schema — connectors.json (root)
Questo è l'**unico punto di fiducia**. Contiene `type`, `scope` e gli sha256 dei file di ogni connector.
Non ha hash di sé stesso — in futuro potrà essere firmato digitalmente.
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
{
@@ -192,8 +121,6 @@ Non ha hash di sé stesso — in futuro potrà essere firmato digitalmente.
"version_string": "1.0.0",
"version_release_date": "2026-07-19",
"files": [
| `auth` | per OAuth | Oggetto con `type`, `provider`, `scopes` per badge UI (NO `deliver` qui, è nel manifest) |
{"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},
@@ -205,28 +132,28 @@ Non ha hash di sé stesso — in futuro potrà essere firmato digitalmente.
}
```
### Campi dell'indice
### Index fields
| Campo | Obbligatorio | Descrizione |
|-------|-------------|-------------|
| `id` | ✅ | Identificatore unico (kebab-case) |
| `name` | ✅ | Nome visualizzato |
| `type` | ✅ | `mcp_remote` o `mcp_local` |
| `scope` | ✅ | `global` o `user` |
| `icon_small` | ✅ | Path relativo dalla root del marketplace |
| `icon_large` | ✅ | Path relativo dalla root del marketplace |
| `user_description` | ✅ | Descrizione breve per la UI |
| `requires` | ✅ | Array di enum requisiti |
| `tags` | ✅ | Array di tag per filtraggio |
| `folder` | ✅ | Nome della cartella del connector |
| `version` | ✅ | Intero per-connector, +1 a ogni modifica dei file |
| `version_string` | ✅ | Semver (solo display) |
| `version_release_date` | ✅ | Data ISO 8601 YYYY-MM-DD (solo display) |
| `files` | ✅ | Array di file con sha256 (NO self-hash) |
| 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 cartella)
## Schema — connector.json (per folder)
Configurazione tecnica per l'attivazione del connector.
Technical configuration for connector activation.
```json
{
@@ -280,63 +207,63 @@ Configurazione tecnica per l'attivazione del connector.
}
```
### Campi del connector.json
### connector.json fields
| Campo | Obbligatorio | Descrizione |
|-------|-------------|-------------|
| `id` | ✅ | Identificatore unico (match con folder name) |
| `name` | ✅ | Nome visualizzato |
| `version` | ✅ | Intero per-connector, +1 a ogni modifica dei file |
| `version_string` | ✅ | Semver (solo display) |
| `version_release_date` | ✅ | Data ISO 8601 YYYY-MM-DD (solo display) |
| `type` | ✅ | `mcp_remote` o `mcp_local` |
| `scope` | ✅ | `global` o `user` |
| `requires` | ✅ | Array di enum requisiti |
| `tags` | ✅ | Array di tag |
| `auth` | ✅ | Oggetto configurazione autenticazione |
| `docs` | ✅ | Array di documentazione multilingua. **`llm_short_description`** è il campo che finisce nel system prompt dell'LLM — deve descrivere COSA FA il connector, non elencare i tool (l'LLM li vede dopo `activate_tools`). Esempio: *"Weather — current conditions, 16-day forecast, and AQI data for any location."*
| `icon_small` | ✅ | Nome file icona nella cartella locale |
| `icon_large` | ✅ | Nome file icona nella cartella locale |
| `launch_command` | solo `mcp_local` | Comando per avviare il server MCP |
| `transport` | solo `mcp_local` | `stdio` (default) |
| `dependencies` | consigliato | Dipendenze Python/Node (array vuoto se solo stdlib) |
| `env` | se `requires` include `ENV` | Variabili d'ambiente che l'utente deve fornire (schema per la UI) — vedi § Campo env |
| `setup_instructions` | consigliato | Passi per configurare il connector |
| `mcp_config` | solo `mcp_local` | Configurazione per l'MCP client |
| `homepage` | opzionale | URL del servizio |
| 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 |
## Enum riservati
## Reserved enums
### type (tipo di connector)
### type (connector type)
| Valore | Descrizione | Esempi |
|--------|-------------|--------|
| `mcp_remote` | Server MCP hosted, accessibile via URL | Tavily, Weather |
| `mcp_local` | Script da eseguire localmente | Gmail, Google Calendar, WhatsApp |
| `script` | Script standalone (non MCP) | *(futuro)* |
| 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 (ambito di configurazione)
### scope (configuration scope)
| Valore | Descrizione | Esempi |
|--------|-------------|--------|
| `global` | Una singola istanza/config per tutto il sistema | Tavily, Weather, Google Trends |
| `user` | Ogni utente ha la propria istanza/autenticazione | Gmail, WhatsApp, Google Calendar |
| 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 (prerequisiti)
### requires (prerequisites)
| Valore | Descrizione |
|--------|-------------|
| `API_KEY` | Richiede una chiave API da configurare |
| `OAUTH` | Richiede autenticazione OAuth (Google, ecc.) |
| `DOCKER` | Richiede Docker Engine |
| `NODE` | Richiede Node.js runtime |
| `PYTHON` | Richiede Python 3 |
| `SECRETS_DIR` | ❌ **Deprecato**la cartella `secrets/` è rimossa dal modello; i connector devono usare `ENV`/`SECRET` (vedi § Sintassi placeholder) |
| `ENV` | Richiede variabili d'ambiente (dichiarate nel campo `env` del manifest) |
| 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) |
## Campo auth
## The auth field
Struttura che descrive come il connector gestisce l'autenticazione:
Structure describing how the connector handles authentication:
```json
// API key in query string
@@ -345,112 +272,113 @@ Struttura che descrive come il connector gestisce l'autenticazione:
// API key in header
{"type": "api_key", "delivery": "header", "param": "X-API-Key"}
// OAuth2 — provider SOLO slug (Skald risolve endpoint + client secrets)
// OAuth2 — provider is ONLY a slug (Skald resolves endpoints + client secrets)
{"type": "oauth2", "provider": "google", "scopes": ["...", "..."]}
// OAuth2 con deliver (Skald inietta il JSON authorized_user via env var)
// 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 con deliver su file (legacy)
// 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 fornita via variabili d'ambiente
// Password / app-password provided via environment variables
{"type": "password", "delivery": "env"}
// Nessuna autenticazione
// No authentication
{"type": "none"}
```
### Campo deliver (solo OAuth2)
### The deliver field (OAuth2 only)
Dichiara **come** Skald consegna la credenziale OAuth ottenuta al processo del server MCP.
Declares **how** Skald delivers the obtained OAuth credential to the MCP server process.
| Campo | Obbligatorio | Descrizione |
|-------|-------------|-------------|
| `as` | ✅ | `"file"` (su disco) o `"env"` (variabile d'ambiente) |
| `format` | ✅ | Nome della serializzazione — es. `"google_authorized_user"` (JSON Google che `from_authorized_user_file` legge), `"refresh_token"`, `"access_token"` |
| `path` | solo `as=file` | Path con placeholder `{secrets}` (Skald lo espande a dir per-utente a runtime). DEVE matchare il path in `mcp_config.env`. |
| `env` | solo `as=env` | Nome della variabile d'ambiente in cui Skald inietta l'intero JSON authorized_user. **Non va dichiarata in `mcp_config.env`** — Skald la inietta a runtime. |
| 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. |
Il feed NON contiene MAI: `client_id`, `client_secret`, `endpoint` URL, `redirect_uri`. Questi sono risolti lato Skald a partire dal nome del `provider`.
The feed NEVER contains: `client_id`, `client_secret`, `endpoint` URL, `redirect_uri`. These are resolved on the Skald side from the `provider` name.
## Campo env (variabili d'ambiente)
## The env field (environment variables)
Usato quando `requires` include `ENV`. È un **array** che dichiara le variabili
d'ambiente che l'utente deve fornire per far funzionare il connector; **nessuna
credenziale finisce su disco né in `secrets/`** — l'host raccoglie i valori,
obbliga la compilazione dei campi obbligatori, e li inietta come environment
nel processo del server MCP al lancio. Il server le legge da `os.environ`.
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", // nome della variabile d'ambiente
"label": "IMAP host", // etichetta per la UI
"description": "IMAP server hostname (es. imap.gmail.com)",
"required": true, // se true, l'host deve obbligare la compilazione
"secret": false, // se true, la UI la maschera e la tratta come segreto
"example": "imap.gmail.com" // placeholder/esempio (opzionale)
"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 o app-password del provider",
"description": "Password or app-password of the provider",
"required": true,
"secret": true,
"default": "" // valore di default se non obbligatorio (opzionale)
"default": "" // default value if not required (optional)
}
]
```
| Campo | Obbligatorio | Descrizione |
|-------|-------------|-------------|
| `name` | ✅ | Nome della variabile d'ambiente (UPPER_SNAKE_CASE) |
| `label` | ✅ | Etichetta breve per la UI |
| `description` | ✅ | Testo di aiuto |
| `required` | ✅ | Se `true`, l'host obbliga l'utente a fornire un valore |
| `secret` | consigliato | Se `true`, valore sensibile (mascherato, non loggato) |
| `default` | opzionale | Valore usato se non fornito (solo per non obbligatorie) |
| `example` | opzionale | Placeholder di esempio per la UI |
| 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 |
## Sintassi placeholder (unificata)
## Placeholder syntax (unified)
Ogni valore che skald deve riempire a runtime con un dato fornito dall'utente
usa **uno di due token**, ovunque compaia (URL, `mcp_config.env`, `verify.command`):
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 | Significato | Esempio |
|-------|-------------|---------|
| `{ENV:NAME}` | Variabile non sensibile (hostname, porta, username…) | `{ENV:EMAIL_IMAP_HOST}` |
| `{SECRET:NAME}` | Variabile sensibile (password, API key, token) | `{SECRET:EMAIL_PASSWORD}` |
| 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` è il nome dichiarato nell'array `env[]` (campo `name`). skald raccoglie
i valori tramite un form (maschera i `{SECRET:}`), li inietta come environment
nel processo del server MCP / verify, e sostituisce i token nel manifest.
`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.
Regole:
- I token non riconosciuti (es. `{secrets}/…`, legacy `{key}`, `{env:NAME}`)
sono **deprecati**: skald non li sostituisce e il manifest va aggiornato.
- `{SECRET:<auth.param>}` è riservato alla chiave primaria quando
`auth.type = "api_key"` (es. Tavily: `{SECRET:tavilyApiKey}`). skald tratta
quel valore anche come API key per il routing bearer/header.
- Un token `{ENV:X}` o `{SECRET:X}` la cui `X` non è nell'`env[]` del manifest
viene sostituito con stringa vuota (l'host non può indovinarlo).
Rules:
- Unrecognized tokens (e.g. `{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: `{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 | Stato | Sostituzione |
|-------|-------|--------------|
| `{key}` | ❌ deprecato | `{SECRET:<auth.param>}` |
| `{env:NAME}` | ❌ deprecato | `{ENV:NAME}` |
| `{secrets}/…` | ❌ deprecato | Il connector deve dichiarare il path come `{ENV:…}` (la cartella `secrets/` è rimossa dal modello) |
| Token | Status | Replacement |
|-------|--------|-------------|
| `{key}` | ❌ deprecated | `{SECRET:<auth.param>}` |
| `{env:NAME}` | ❌ deprecated | `{ENV:NAME}` |
| `{secrets}/…` | ❌ deprecated | The connector must declare the path as `{ENV:…}` (the `secrets/` folder is removed from the model) |
## Campo verify (test prima del salvataggio)
## The verify field (test before save)
Dichiara un comando shell che skald esegue **dopo** che l'utente ha compilato
il form e **prima** di persistere l'attivazione. Serve a verificare che le
credenziali appena inserite funzionino davvero.
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": {
@@ -459,174 +387,145 @@ credenziali appena inserite funzionino davvero.
}
```
| Campo | Obbligatorio | Descrizione |
|-------|-------------|-------------|
| `command` | ✅ | Comando shell. Gira nello stesso sandbox del server: container `skald-{userid}` per `mcp_local` user, host per `mcp_remote` global. Le env/secret dichiarati sono iniettate |
| `timeout_secs` | opzionale | Default 15. skald killa il processo allo scadere |
| 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 |
### Convenzione output
### Output convention
Il comando deve stampare **un singolo oggetto JSON su stdout** e nient'altro:
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"}
```
| Campo | Tipo | Descrizione |
| Field | Type | Description |
|-------|------|-------------|
| `ok` | bool | `true` = test passato |
| `message` | string | Messaggio mostrato all'utente (mai loggare secret qui dentro) |
| `details` | object | Opzionale, dettagli strutturati mostrati in `<pre>` |
| `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 su successo, ≠ 0 su fallimento (skald usa l'exit code come
fallback se il parse JSON fallisce). **Mai stampare credenziali** nel
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`.
### Dove mettere lo script
### Where to put the script
Se `command` referenzia un file (es. `verify.py`), il file va:
1. Aggiunto all'array `files[]` in `connectors.json` (con `sha256` e `size`)
2. Salvato nella cartella del connector (`<id>/verify.py`)
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 (`<id>/verify.py`)
skald lo scarica, ne verifica lo SHA-256 contro l'indice, e lo rende disponibile
nello stesso path del server principale (container per i `mcp_local`, dir
`./scripts/<id>/` sull'host per i `mcp_remote`).
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/<id>/` dir on the host for `mcp_remote` ones).
### Senza verify
### Without verify
Se `verify` manca, skald **non esegue nessun test**l'attivazione è diretta
come oggi. Il connector va in `auth_state='ready'` senza verifica. Per i
`mcp_remote` non c'è fallback handshake: l'autore del manifest decide se vuole
il test scrivendo `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`.
## Convenzioni icone
## Icon conventions
- **Formato**: SVG per icone vettoriali (meglio per retina/zoom), PNG per raster
- **Nome**: `icon_sm.{svg|png}` (small, ~48×48px), `icon_lg.{svg|png}` (large, ~96×96px)
- **Path**: relativo alla cartella del connector
- **Nell'indice** la path è `{folder}/{filename}` (es. `gmail/icon_sm.svg`)
- **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`)
## Integrità file (sha256)
## File integrity (sha256)
- Gli SHA-256 sono generati **automaticamente** da `scripts/compile.py` a partire
dai file fisici presenti in ogni cartella — mai manuali, mai stale.
- L'unico file firmato (in futuro) sarà `connectors.json` (l'indice).
- `fragment.json` e `index.json` non hanno hash — sono solo input di compilazione.
- 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.
## Struttura directory
## Local workflow
```
connectors/
├── index.json ← lista ordinata degli id dei connector (input per compile.py)
├── connectors.json ← INDICE COMPILATO (generato da compile.py, non editare)
├── compile.py ← genera connectors.json (lanciato da scripts/compile.py)
├── index.html ← Catalogo UI (legge connectors.json via fetch)
├── oauth/
│ └── show.html ← OAuth callback receiver
├── gmail/ ← Un connector per cartella
│ ├── fragment.json ← Frammento dell'indice (id, name, type, ..., SENZA files[])
│ ├── connector.json ← Configurazione tecnica (mcp_config, auth.deliver, ...)
│ ├── gmail_mcp_server.py ← Script MCP
│ ├── gmail_oauth_setup.py ← Script setup OAuth
│ ├── requirements.txt ← Dipendenze Python
│ ├── icon_sm.svg ← Icona piccola (48×48)
│ └── icon_lg.svg ← Icona grande (es. 96×96)
├── email/
│ ├── fragment.json
│ ├── connector.json
│ ├── email_mcp_server.py
│ ├── verify.py
│ ├── requirements.txt
│ ├── icon_sm.svg
│ └── icon_lg.svg
└── ...
```
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, ...)
- Add the MCP script, icons, verify.py, requirements.txt
- Add the id to `connectors/index.json`
- Run `python3 scripts/compile.py`
## Workflow locale
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`
1. **Aggiungere un nuovo connector**:
- Crea cartella `connectors/<id>/`
- Crea `fragment.json` (id, name, type, scope, icone, auth, tools, ...)
- Crea `connector.json` (config tecnica: mcp_config, launch_command, ...)
- Aggiungi script MCP, icone, verify.py, requirements.txt
- Aggiungi l'id a `connectors/index.json`
- Lancia `python3 scripts/compile.py`
2. **Modificare un connector esistente**:
- Modifica i file nella cartella del connector
- **Non toccare** `connectors.json` — viene rigenerato
- Lancia `python3 scripts/compile.py`
3. **Prima del deploy**:
3. **Before deploying**:
```bash
python3 scripts/compile.py # rigenera connectors.json con SHA-256 fresh
python3 scripts/compile.py --verify # (opzionale) verifica che sia aggiornato
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 sul server**:
4. **Deploy to the server**:
```bash
ssh dguiducci@skald-home-server /home/dguiducci/marketplace_deploy.sh
```
oppure con MCP SSH:
or with MCP SSH:
```bash
# via mcp__ssh__exec alias "skald-home-server"
/home/dguiducci/marketplace_deploy.sh
```
(Il deploy script fa `git pull` + `cp -r connectors/* /var/www/connectors.skaldagent.net/`)
(The deploy script does `git pull` + `cp -r connectors/* /var/www/connectors.skaldagent.net/`)
5. Verifica su `https://connectors.skaldagent.net/`
5. Verify on `https://connectors.skaldagent.net/`
## Fragment.json
Vedi [docs/connector.manifest_guide.md](docs/connector.manifest_guide.md) per la guida completa
alla creazione.
See [docs/connector.manifest_guide.md](docs/connector.manifest_guide.md) for the complete
creation guide.
`fragment.json` contiene tutti i campi dell'entry di `connectors.json` **tranne** `files[]`.
Questi sono i campi obbligatori:
`fragment.json` contains all the fields of a `connectors.json` entry **except** `files[]`.
These are the required fields:
| Campo | Obbligatorio | Descrizione |
|-------|-------------|-------------|
| `id` | ✅ | Identificatore unico (match con folder name) |
| `name` | ✅ | Nome visualizzato |
| `type` | ✅ | `mcp_remote` o `mcp_local` |
| `scope` | ✅ | `global` o `user` |
| `icon_small` | ✅ | Path relativo dalla root del marketplace |
| `icon_large` | ✅ | Path relativo dalla root del marketplace |
| `user_description` | ✅ | Descrizione breve per la UI |
| `requires` | ✅ | Array di enum requisiti |
| `tags` | ✅ | Array di tag per filtraggio |
| `folder` | ✅ | Nome della cartella del connector (match con `id`) |
| `version` | ✅ | Intero per-connector, +1 a ogni modifica dei file |
| `version_string` | ✅ | Semver (solo display) |
| `version_release_date` | ✅ | Data ISO 8601 (solo display) |
| `tools` | ✅ | Array di `{name, display_name}` per friendly names UI |
| `auth` | opzionale | Configurazione autenticazione (se diversa da `"none"`) |
| 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"`) |
Nota: l'array `files[]` (con SHA-256 e size) viene aggiunto **automaticamente**
da `compile.py` scansionando i file presenti nella cartella — non va mai scritto
a mano.
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 su server remoto
## Deploy to remote server
Il server remoto è:
The remote server is:
- **Host**: skald-home-server (192.168.1.100 / 145.40.169.107)
- **User**: dguiducci
- **Path**: `/var/www/connectors.skaldagent.net/`
- **Proprietario**: `caddy:caddy`
- **Sudo**: richiesto per scrivere in `/var/www/`
- **Owner**: `caddy:caddy`
- **Sudo**: required to write in `/var/www/`
## Connector attuali
## Current connectors
| ID | Nome | Tipo | Scope | Auth | Verify |
| 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`) | ⏳ Fase 2 — OAuth via loopback listener |
| `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) |
@@ -637,6 +536,8 @@ Il server remoto è:
| `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 | — |
**Stato del verify-before-save in skald**: `exa`, `drive`, `email`, `tavily`, e `gcal` hanno `verify` completo
(script + JSON output); `gmail` aspetta la Fase 2 (OAuth via loopback listener).
Un connector senza `verify` viene attivato senza test — vedi § Senza verify.
| `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.