# Skald Connectors Marketplace _Updated: 2026-07-21_ **Remote**: `https://git.skaldagent.net/dguiducci/skald-connectors.git` **Live**: `https://connectors.skaldagent.net/` **OAuth callback**: `https://connectors.skaldagent.net/oauth/show.html` ## 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. Per deployare l'ultima versione del marketplace sul server: ```bash ssh dguiducci@skald-home-server /home/dguiducci/marketplace_deploy.sh ``` Lo script sul server fa: 1. `git pull` in `/home/dguiducci/repos/skald-connectors/` 2. `cp -r connectors/*` in `/var/www/connectors.skaldagent.net/` La directory `/var/www/connectors.skaldagent.net/` è di proprietà di `dguiducci`, quindi non serve sudo per la copia. ## 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: 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`). **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") Stato attuale (2026-07-21): tutti i 13 connector del marketplace hanno `title` nello script o `tools[]` nel manifest. ## Struttura directory ``` connectors/ ├── connectors.json ← INDICE (radice di fiducia unica) ├── index.html ← Catalogo UI (legge connectors.json via fetch) ├── oauth/ │ └── show.html ← OAuth callback receiver (client-side, no backend) ├── gmail/ ← Un connector per cartella │ ├── 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. preview) ├── email/ │ ├── connector.json │ ├── email_mcp_server.py │ ├── verify.py │ ├── requirements.txt │ ├── icon_sm.svg │ └── icon_lg.svg ├── ssh/ │ ├── connector.json │ ├── ssh_mcp_server.py │ ├── requirements.txt │ ├── icon_sm.svg │ └── icon_lg.svg └── tavily/ ├── connector.json ├── verify.py ├── icon_sm.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. ```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": [ | `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}, {"path": "icon_sm.svg", "sha256": "029d7f5d81de6cf2b17b...", "size": 251}, {"path": "requirements.txt", "sha256": "3f659cc5e5f0543f1326...", "size": 82} ] } ] } ``` ### Campi dell'indice | 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) | ## Schema — connector.json (per cartella) Configurazione tecnica per l'attivazione del connector. ```json { "id": "gmail", "name": "Gmail", "version": 1, "version_string": "1.0.0", "version_release_date": "2026-07-19", "type": "mcp_local", "scope": "user", "launch_command": "python3 gmail_mcp_server.py", "transport": "stdio", "requires": ["OAUTH", "PYTHON"], "tags": ["email", "mcp", "local", "google"], "dependencies": [ "google-api-python-client>=2.150.0", "google-auth>=2.35.0", "google-auth-oauthlib>=1.2.0" ], "setup_instructions": [ "Install dependencies: pip install -r requirements.txt", "Run: python3 gmail_oauth_setup.py (optional, for standalone use — Skald handles OAuth)" ], "docs": [ { "lang": "en", "description": "Full description for human users...", "llm_short_description": "One-line description for LLM context..." } ], "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" } ``` ### Campi del connector.json | 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 | | `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 | ## Enum riservati ### type (tipo di connector) | 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)* | ### scope (ambito di configurazione) | 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 | ### requires (prerequisiti) | 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) | ## Campo auth Struttura che descrive come il connector gestisce l'autenticazione: ```json // API key in query string {"type": "api_key", "delivery": "query", "param": "tavilyApiKey"} // API key in header {"type": "api_key", "delivery": "header", "param": "X-API-Key"} // OAuth2 — provider SOLO slug (Skald risolve endpoint + client secrets) {"type": "oauth2", "provider": "google", "scopes": ["...", "..."]} // OAuth2 con deliver (Skald inietta il JSON authorized_user 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) {"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 {"type": "password", "delivery": "env"} // Nessuna autenticazione {"type": "none"} ``` ### Campo deliver (solo OAuth2) Dichiara **come** Skald consegna la credenziale OAuth ottenuta al processo del server MCP. | 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. | Il feed NON contiene MAI: `client_id`, `client_secret`, `endpoint` URL, `redirect_uri`. Questi sono risolti lato Skald a partire dal nome del `provider`. ## Campo env (variabili d'ambiente) 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`. ```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_PASSWORD", "label": "Password / app password", "description": "Password o app-password del provider", "required": true, "secret": true, "default": "" // valore di default se non obbligatorio (opzionale) } ] ``` | 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 | ## Sintassi placeholder (unificata) 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`): | 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}` | `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. Regole: - I token non riconosciuti (es. `{secrets}/…`, legacy `{key}`, `{env:NAME}`) sono **deprecati**: skald non li sostituisce e il manifest va aggiornato. - `{SECRET:}` è 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). ### Deprecations | Token | Stato | Sostituzione | |-------|-------|--------------| | `{key}` | ❌ deprecato | `{SECRET:}` | | `{env:NAME}` | ❌ deprecato | `{ENV:NAME}` | | `{secrets}/…` | ❌ deprecato | Il connector deve dichiarare il path come `{ENV:…}` (la cartella `secrets/` è rimossa dal modello) | ## Campo verify (test prima del salvataggio) 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. ```json "verify": { "command": "python3 verify.py", "timeout_secs": 20 } ``` | 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 | ### Convenzione output Il comando deve stampare **un singolo oggetto JSON su stdout** e nient'altro: ```json {"ok": true, "message": "IMAP and SMTP authentication successful", "details": {"imap": "...", "smtp": "..."}} {"ok": false, "message": "IMAP login failed: INVALID_CREDENTIALS"} ``` | Campo | Tipo | Descrizione | |-------|------|-------------| | `ok` | bool | `true` = test passato | | `message` | string | Messaggio mostrato all'utente (mai loggare secret qui dentro) | | `details` | object | Opzionale, dettagli strutturati mostrati in `
` |

Exit code: 0 su successo, ≠ 0 su fallimento (skald usa l'exit code come
fallback se il parse JSON fallisce). **Mai stampare credenziali** nel
`message`/`details`.

### Dove mettere lo 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 (`/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//` sull'host per i `mcp_remote`).

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

## Convenzioni icone

- **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`)

## Integrità file (sha256)

- Gli hash sha256 sono **solo in `connectors.json`** (l'indice)
- `connector.json` **non contiene hash di sé stesso** — è il file manifesto
- L'indice è l'unica radice di fiducia; in futuro si può firmare digitalmente solo l'indice
- Sui file locali, per calcolare/aggiornare gli hash:
  ```bash
  python3 -c "import hashlib; print(hashlib.sha256(open('file.py','rb').read()).hexdigest())"
  ```

## Workflow locale

1. Lavora su file nella cartella `connectors/`
2. Modifica `connectors.json`, `connector.json`, script, icone
3. **Prima del deploy** aggiorna gli sha256 in `connectors.json`:
   ```bash
   python3 scripts/update_hashes.py
   ```
4. Fai l'upload sul server con rsync:
   ```bash
   rsync -avz --delete connectors/ dguiducci@skald-server:/var/www/connectors.skaldagent.net/
   ```
5. Verifica su `https://connectors.skaldagent.net/`

## Deploy su server remoto

Il server remoto è:

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

Comando di deploy (da eseguire con sudo o con rsync):

```bash
# Sync se il server permette rsync via SSH
rsync -avz --delete ./connectors/ dguiducci@skald-server:/var/www/connectors.skaldagent.net/
```

Dopo il deploy, verificare i permessi:
```bash
sudo chown -R caddy:caddy /var/www/connectors.skaldagent.net/
sudo find /var/www/connectors.skaldagent.net/ -type f -exec chmod 644 {} \;
```

## Connector attuali

| ID | Nome | Tipo | Scope | Auth | Verify |
|----|------|------|-------|------|--------|
| `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 |
| `gcal` | Google Calendar | `mcp_local` | `user` | oauth2 (Google) + deliver: `env/google_authorized_user` (via `GCAL_CREDS_JSON`) | `verify.py` (creds load + API probe) |
| `drive` | Google Drive | `mcp_local` | `user` | oauth2 (Google) + deliver: `env/google_authorized_user` (via `DRIVE_CREDS_JSON`) | `verify.py` (creds load + Drive API probe) |
| `email` | Email (IMAP/SMTP) | `mcp_local` | `user` | password (env) | `verify.py` (IMAP+SMTP probe) |
| `firecrawl` | Firecrawl | `mcp_local` | `global` | api_key (env) | — |
| `http-fetch` | HTTP Fetch | `mcp_local` | `global` | none | — |
| `ssh` | SSH Remote Access | `mcp_local` | `user` | none (auth runtime per-alias) | — |
| `weather` | Weather (Open-Meteo) | `mcp_local` | `global` | none | — |
| `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.