- 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
23 KiB
Skald Connectors Marketplace
What it is
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.).
Two types of connectors:
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).
Local references
- 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/
OAuth callback: https://connectors.skaldagent.net/oauth/show.html
Deploy
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.
Quick deploy with MCP SSH:
mcp__ssh__exec alias=skald-home-server command="/home/dguiducci/marketplace_deploy.sh"
Or via classic SSH:
ssh dguiducci@skald-home-server /home/dguiducci/marketplace_deploy.sh
The script on the server does:
git pullin/home/dguiducci/repos/skald-connectors/cp -r connectors/*into/var/www/connectors.skaldagent.net/
The /var/www/connectors.skaldagent.net/ directory is owned by dguiducci,
so no sudo is needed for the copy.
Before deploying, remember to regenerate the index:
python3 scripts/compile.py
Friendly tool names (2026-07-21)
Every MCP tool must expose a friendly name for the Skald UI. Two ways, in order of preference:
- Via the MCP script (preferred) — add
"title": "Friendly Name"in the definition of each tool insidetools/list. Works for all local scripts (Python/Node) that we control. - Via the manifest (fallback) — add
"tools": [{"name": "...", "display_name": "..."}]inconnector.jsonand inconnectors.json. Used only for remote connectors or external packages (e.g.npx -y firecrawl-mcp).
Resolution order (Skald tries them in this order):
tools[].display_namefrom the manifesttitlefrom the MCP server'stools/list- Automatic prettify of the raw name (
send_message→ "Send Message")
Current status (2026-07-21): all 13 marketplace connectors have title in the script or tools[] in the manifest.
Directory structure
connectors/
├── 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
├── 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
│ ├── email_mcp_server.py
│ ├── verify.py
│ ├── requirements.txt
│ ├── icon_sm.svg
│ └── icon_lg.svg
└── ...
Schema — connectors.json (root)
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.
{
"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": "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}
]
}
]
}
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 files with sha256 (NO self-hash) |
Schema — connector.json (per folder)
Technical configuration for connector activation.
{
"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": "Google Calendar — read, create, update, delete, and respond to calendar events. 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 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 |
Reserved enums
type (connector type)
| 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 (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 |
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) |
The auth field
Structure describing how the connector handles authentication:
// 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 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"}
// 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, endpoint URL, redirect_uri. These are resolved on the Skald side from the provider name.
The env field (environment variables)
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.
"env": [
{
"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 or app-password of the provider",
"required": true,
"secret": true,
"default": "" // default value if not required (optional)
}
]
| 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 |
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 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.
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 whenauth.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 whoseXis not in the manifest'senv[]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 | The connector must 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 has filled in the form and before persisting the activation. It checks that the credentials just entered actually work.
"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:
{"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 the
message/details.
Where to put the script
If command references a file (e.g. verify.py), the file must:
- Be added to the
files[]array inconnectors.json(withsha256andsize) - Be saved in the connector folder (
<id>/verify.py)
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).
Without 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.
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 the index the path is
{folder}/{filename}(e.g.gmail/icon_sm.svg)
File integrity (sha256)
- SHA-256 hashes are generated automatically by
scripts/compile.pyfrom 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.jsonandindex.jsonhave no hash — they are just compilation inputs.
Local workflow
-
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
- Create the folder
-
Modifying an existing connector:
- Edit the files in the connector folder
- Do not touch
connectors.json— it gets regenerated - Run
python3 scripts/compile.py
-
Before deploying:
python3 scripts/compile.py # regenerates connectors.json with fresh SHA-256s python3 scripts/compile.py --verify # (optional) verifies that it is up to date -
Deploy to the server:
ssh dguiducci@skald-home-server /home/dguiducci/marketplace_deploy.shor with MCP SSH:
# via mcp__ssh__exec alias "skald-home-server" /home/dguiducci/marketplace_deploy.sh(The deploy script does
git pull+cp -r connectors/* /var/www/connectors.skaldagent.net/) -
Verify on
https://connectors.skaldagent.net/
Fragment.json
See docs/connector.manifest_guide.md for the complete creation guide.
fragment.json contains all the fields of a connectors.json entry except files[].
These are the required fields:
| 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") |
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 to remote server
The remote server is:
- Host: skald-home-server (192.168.1.100 / 145.40.169.107)
- User: dguiducci
- Path:
/var/www/connectors.skaldagent.net/ - Owner:
caddy:caddy - Sudo: required to write in
/var/www/
Current connectors
| 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) |
⏳ 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) |
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 | — |
google-trends |
Google Trends | mcp_local |
global |
none | verify.py (trendspyg import probe) |
wikipedia |
Wikipedia | mcp_local |
global |
none | — |
whatsapp |
mcp_local |
user |
qr | — | |
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.