The connector asked for a sudo password on every privileged call and failed whenever nobody answered it, which is every unattended run. - Always probe `sudo -n` first, even for aliases set to sudo="prompt". `_sudo_prefix` used to elicit unconditionally, so a host granting this user NOPASSWD still opened an Agent Inbox prompt; with no human there it hit the client's 300s ELICITATION_DEADLINE, got back `cancel`, and surfaced as "sudo password required (user declined or timed out)". sudo refuses before running anything when it wants a password, so the probe is side-effect free. - Strip a leading `sudo` from `command` and turn it into sudo=true. Agents write `exec(command="sudo systemctl restart x")`: with sudo=false that ran a tty-less sudo, with sudo=true it nested `sudo -S ... sudo ...` whose inner prompt had no tty either. Handles -u/-n/-S/-E/-H/-i/-k/-p/--; an unknown flag leaves the command alone. sudo_user now implies sudo=true. - Run privileged commands as `sh -c '<command>'`, so `&&`, pipes and redirections are elevated too instead of only the first word. - Add SSH_MCP_SUDO_PASSWORD (optional, secret) for unattended runs. It is consulted only after `sudo -n` proved a password is needed, so on a NOPASSWD host it never lands in the command's own stdin. - Actionable errors for every sudo failure mode, and a `hint` on a nested sudo we could not peel off. General review of the same server: - Drain stdout and stderr together and make timeout_sec a real wall-clock deadline. Both streams share one SSH channel window, so reading stdout to EOF first stalled once a chatty stderr filled it. Command stdin is now closed after the optional password. - Queue messages that arrive while awaiting an elicitation reply instead of discarding them, so a concurrent tools/call is not lost. - Record the client's `elicitation` capability at initialize and fail fast when it is absent rather than blocking on a prompt nobody can answer. - Tolerate null/string integer arguments (depth, max_results, context_lines, timeout_sec). - Realign the version across both manifests: fragment.json said 5/1.0.4 while connector.json said 2/1.0.1, so the feed was permanently ahead of the installed version and offered an update forever. Also lands the pending docs work: CONNECTOR_MANIFEST_GUIDE.md as the single source of truth, docs/connector.manifest_guide.md retired to a pointer, CLAUDE.md audited against the repo, compile.py docstring fixed, and an opencode.json config.
33 KiB
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 (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(branchmain) - 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 consuming project
The application that consumes this marketplace — the code that installs, verifies, and runs these
connectors — lives in a separate repository at ~/projects/skald-circle (Rust workspace: crates/,
Cargo.toml, blueprint/, agents/). It is the client for everything documented here: it fetches
connectors.json, checks each file against its pinned SHA-256, resolves auth / env / verify, and
launches the MCP servers. The parts worth reading when a manifest question comes up:
| Question | File in ~/projects/skald-circle |
|---|---|
| How the feed is parsed, hashed, installed | src/frontend/api/marketplace.rs |
| Activation, the env form, OAuth/QR handoff | src/frontend/api/mcp.rs |
| Server spec, URL placeholders, transports | crates/skald-core/src/mcp/mod.rs |
| Folder layout, container copy, deps install | crates/skald-core/src/mcp/install.rs |
Verify runner + {ENV:}/{SECRET:} engine |
crates/skald-core/src/mcp/verify.rs |
It no longer keeps a copy of the spec: it references CONNECTOR_MANIFEST_GUIDE.md in this repo, which is the single source of truth for the connector format. Edit the format here and nowhere else.
The spec
The marketplace spec ships inside this project: CONNECTOR_MANIFEST_GUIDE.md
at the repo root is the authoritative, step-by-step specification for producing a correct connector —
the folder layout, the three source documents + the compiler, which document the client reads each field
from, the MCP-over-stdio server contract, friendly tool names, placement & risk vocabulary, the
auth.type variants, the env[] form and placeholder engines, dependency installation,
verify-before-save, versioning, the limits the client enforces, and the new-connector checklist. Give
this file to any agent that generates new connectors, and update it whenever the connector format
changes — it is the only copy, referenced by ~/projects/skald-circle rather than duplicated there.
docs/connector.manifest_guide.md is a retired copy, now a pointer to the root guide. Do not edit it.
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 forcompile.py.connectors/<id>/fragment.json— the connector's index entry withoutfiles[](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 connectorid.connectors/connectors.json— the compiled index and single root of trust. Generated byscripts/compile.pyfromindex.json+ eachfragment.json+ a physical scan of every connector folder. It lists every connector with afiles[]array carrying thesha256+sizeof 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.jsonat an absolute path and links to/<folder>/, so it only works when served from the site root (not opened as afile://).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)
{
"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 |
optional | Array of {name, display_name} for friendly UI names. Documentary here — Skald reads tools[] from connector.json (see § Friendly tool 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.
{
"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:
// 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 — ❌ NOT IMPLEMENTED: Skald rejects `as: "file"` at
// activation with an explicit error. Never ship it.
{"type": "oauth2", "provider": "google", "scopes": ["…"],
"deliver": {"as": "file", "format": "google_authorized_user", "path": "{secrets}/gmail_creds.json"}}
// Password / app-password provided via environment variables — ⚠️ `password` is NOT one of
// the values Skald recognizes (`none`/`api_key`/`oauth2`/`qr`/`ssh_key`); it normalizes to
// `none`. `email` works only because its credentials travel through `env[]`. Prefer
// `api_key` or `none` for new connectors.
{"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 |
✅ | "env" (environment variable). "file" parses but is rejected at activation — unimplemented |
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 |
Unused while file delivery is unimplemented |
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.
"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:
| 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) and injects them as environment into the MCP server / verify process.
Substitution happens in exactly two places — there is no general template engine:
| Where | Engine | Unknown NAME resolves to |
|---|---|---|
mcp_config.url (remote connectors) |
skald-core/src/mcp/mod.rs |
{SECRET:x} falls back to the connector's api_key; otherwise the token is left in the URL literally |
verify.command |
skald-core/src/mcp/verify.rs |
the empty string |
⚠️ mcp_config.env is NOT substituted. What reaches a local server's environment is the form's
env[] values keyed by their name, verbatim — so env[].name must be exactly the variable name the
script reads. The mcp_config.env map survives only as a legacy fallback for the form schema (bare key
names); if it is ever the sole env source its {ENV:…} strings are injected literally. email, ssh
and linkedin still carry such a map — harmless today, but do not rely on it to rename a variable.
Rules:
- Unrecognized tokens (
{secrets}/…,{env:NAME}) are deprecated: skald does not substitute them and the manifest must be updated. Legacy{key}still resolves to the api_key in a URL. {SECRET:<auth.param>}is reserved for the primary key whenauth.type = "api_key"(e.g. Tavily:?tavilyApiKey={SECRET:tavilyApiKey}). skald also treats that value as the API key for bearer/header routing, and stops sending it as a bearer header once it has been spent on the URL.
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.
"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 | Declared for the record, but not yet plumbed through: the runtime applies a fixed 20 s to every verify (VERIFY_TIMEOUT_SECS in src/frontend/api/mcp.rs). Keep the probe well under that |
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 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 finds
the script by matching a basename from files[] against the command string, downloads it, verifies
the hash against the index, and runs it in the connector's own directory — inside the container
skald-{userid} (~/.skald/mcp/<name>/) for a per-user connector, on the host in ./connectors/<id>/
for a global one. A verify command may also be fully inline (see firecrawl, which uses a
node -e "…" one-liner) — with no basename match, nothing extra is fetched.
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:
- Via the MCP script (preferred) — add
"title": "Friendly Name"to each tool definition returned bytools/list. Works for all local scripts (Python/Node) that we control. - Via the manifest (fallback) — add
"tools": [{"name": "…", "display_name": "…"}]toconnector.json. Used only for remote connectors or external packages (e.g.npx -y firecrawl-mcp).
Resolution order used by Skald:
tools[].display_namefrom the manifesttitlefrom the MCP server'stools/list- Automatic prettify of the raw name (
send_message→ "Send Message")
⚠️ Skald reads tools[] from connector.json only — the index entry carries no tools field in
the client's parser, so a block that lives only in fragment.json is inert (that is the state of
google-trends today, which is harmless because its script also sets title). Mirroring the block into
fragment.json is optional and purely documentary.
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_requestimplements the full handshake:initialize→protocolVersion 2024-11-05+serverInfo, a silentnotifications/initialized,ping→{}, andtools/list→{"tools": TOOLS}(an object, not a bare array).- Notifications are never answered: any message without an
idreturnsNone. - Tools are declared as a
TOOLSmanifest list plus aTOOL_DISPATCHmap. - 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_userintoGMAIL_CREDS_JSON; the script also falls back toGMAIL_CREDS_PATHor./secrets/gmail_creds.jsonfor standalone use (thatsecrets/path is deprecated and gitignored). Push = History API polling.verifyis 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 anenv[]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 always probessudo -nfirst (free on a NOPASSWD host, and side-effect free elsewhere since sudo refuses before running the command), then falls back tosudo -Swith a password fromSSH_MCP_SUDO_PASSWORDor MCP elicitation; a leadingsudoinsidecommandis stripped and turned intosudo=true, and under sudo the command runs assh -c '…'so pipes/redirections are privileged too. Elicitation needs a human in the Agent Inbox within 300 s (ELICITATION_DEADLINEinskald-core/src/elicitation/mod.rs) — unattended runs must rely on NOPASSWD orSSH_MCP_SUDO_PASSWORD. No setup-time credentials:auth.type: "none"with noverify. AllSSH_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 |
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 |
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.pyfrom the physical files present in each folder — never manual, never stale. compile.pyexcludes fromfiles[]:fragment.json,connectors.json,index.json,compile.sh,compile.py,update_hashes.py,.DS_Store, and thescripts/,__pycache__/,.git/directories. Everything else in the folder is hashed — includingconnector.json.fragment.jsonandindex.jsonhave 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.jsonby hand. It is generated. Editfragment.json/index.json/ the connector files, then runpython3 scripts/compile.py. - Bump
version(+1) andversion_stringon every file change to a connector, in bothfragment.jsonandconnector.json, and keepversion_release_datecurrent. When the two disagree the manifest wins and Skald only logs amarketplace feed version desyncwarning — and if the index carries the lower number, the strictfeed > installedcomparison can never fire again and the connector silently stops offering updates.compile.pydoes not check this. - Keep
id,name,type,scope,tags,requires, andauthconsistent betweenfragment.jsonandconnector.json. Only the manifest'sauthis parsed by Skald; the index's is documentary, withrequiresas the sole coarse fallback (OAUTH→ oauth,API_KEY→ api_key). - Keep the connector folder flat.
compile.pyscans only the folder's top level, so any file in a subdirectory is silently absent fromfiles[]— never downloaded, never installed. - Adding a
verifyscript 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.pybefore 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
-
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
- Create the folder
-
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
- Edit the files in the connector folder, bump
-
Before deploying:
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):
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):
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:
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 on the server (git pull in
/home/dguiducci/repos/skald-connectors/, then sudo cp -r connectors/* /var/www/connectors.skaldagent.net/):
ssh dguiducci@skald-home-server /home/dguiducci/marketplace_deploy.sh
or via MCP SSH:
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 format (version headings,
Added/Changed/Fixed/Removedcategories,[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.