diff --git a/SKALD.md b/SKALD.md index d0bcf43..5060098 100644 --- a/SKALD.md +++ b/SKALD.md @@ -1,6 +1,10 @@ # Skald Connectors Marketplace -_Updated: 2026-07-21_ +## Reference locale + +- **[docs/connector.manifest_guide.md](docs/connector.manifest_guide.md)** — Guida ufficiale per generare connector corretti (copiata da `skald-circle/blueprint/`) + +_Updated: 2026-07-22_ **Remote**: `https://git.skaldagent.net/dguiducci/skald-connectors.git` **Live**: `https://connectors.skaldagent.net/` @@ -481,6 +485,7 @@ sudo find /var/www/connectors.skaldagent.net/ -type f -exec chmod 644 {} \; | ID | Nome | Tipo | Scope | Auth | Verify | |----|------|------|-------|------|--------| +| `context7` | Context7 | `mcp_remote` | `global` | none | `verify.py` (MCP initialize 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) | diff --git a/connectors/connectors.json b/connectors/connectors.json index 98080ab..6b70289 100644 --- a/connectors/connectors.json +++ b/connectors/connectors.json @@ -838,6 +838,61 @@ "size": 285 } ] + }, + { + "id": "context7", + "name": "Context7", + "type": "mcp_remote", + "scope": "global", + "icon_small": "context7/icon_sm.svg", + "icon_large": "context7/icon_lg.svg", + "user_description": "Up-to-date documentation and code examples for any library, framework, SDK, API, CLI tool, or cloud service \u2014 fetched directly from the source. Free, no API key required.", + "tags": [ + "documentation", + "search", + "mcp", + "remote", + "developer-tools" + ], + "auth": { + "type": "none" + }, + "folder": "context7", + "version": 1, + "version_string": "1.0.0", + "version_release_date": "2026-07-22", + "files": [ + { + "path": "connector.json", + "sha256": "c7dfedad4e017c88420a78420f2ea2de35fc80d921b5da6bf628f8eceaf93584", + "size": 1321 + }, + { + "path": "verify.py", + "sha256": "8e46d562c028c4f5370c22c7df455886836267cf4bbb8ac605c79a2b509fb5f1", + "size": 2094 + }, + { + "path": "icon_sm.svg", + "sha256": "5407c6bf958a7d1c83a76440c8bb8e6508941cae9f9f319dfbb9c9250807f5e0", + "size": 724 + }, + { + "path": "icon_lg.svg", + "sha256": "1f9b3ec611c448eff1c4fb1844a948770f4c4aadf6b7e7a73d07fec738afe209", + "size": 739 + } + ], + "tools": [ + { + "name": "resolve-library-id", + "display_name": "Resolve Library" + }, + { + "name": "query-docs", + "display_name": "Query Docs" + } + ] } ] } \ No newline at end of file diff --git a/connectors/context7/connector.json b/connectors/context7/connector.json new file mode 100644 index 0000000..dfee332 --- /dev/null +++ b/connectors/context7/connector.json @@ -0,0 +1,47 @@ +{ + "id": "context7", + "name": "Context7", + "version": 1, + "version_string": "1.0.0", + "version_release_date": "2026-07-22", + "type": "mcp_remote", + "scope": "global", + "tags": [ + "documentation", + "search", + "mcp", + "remote", + "developer-tools" + ], + "auth": { + "type": "none" + }, + "docs": [ + { + "lang": "en", + "description": "Up-to-date documentation and code examples for any library, framework, SDK, API, CLI tool, or cloud service — fetched directly from the source. Works without an API key on the free tier. Supports library lookup and version-specific documentation retrieval.", + "llm_short_description": "Fetch up-to-date documentation and code examples for libraries and frameworks. Tools: resolve-library-id (look up a library by name), query-docs (retrieve docs for a known library ID)." + } + ], + "mcp_config": { + "url": "https://mcp.context7.com/mcp", + "transport": "streamable-http" + }, + "verify": { + "command": "python3 verify.py", + "timeout_secs": 15 + }, + "homepage": "https://context7.com", + "icon_small": "icon_sm.svg", + "icon_large": "icon_lg.svg", + "tools": [ + { + "name": "resolve-library-id", + "display_name": "Resolve Library" + }, + { + "name": "query-docs", + "display_name": "Query Docs" + } + ] +} diff --git a/connectors/context7/icon_lg.svg b/connectors/context7/icon_lg.svg new file mode 100644 index 0000000..7b0dee8 --- /dev/null +++ b/connectors/context7/icon_lg.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/connectors/context7/icon_sm.svg b/connectors/context7/icon_sm.svg new file mode 100644 index 0000000..6afb268 --- /dev/null +++ b/connectors/context7/icon_sm.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/connectors/context7/verify.py b/connectors/context7/verify.py new file mode 100644 index 0000000..49b7e55 --- /dev/null +++ b/connectors/context7/verify.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +"""Verify connectivity to Context7's hosted MCP server. + +Sends a lightweight JSON-RPC initialize to https://mcp.context7.com/mcp +and checks for a successful response. No API key required — Context7's +remote MCP endpoint works on the free tier without authentication. + +Prints a single JSON object on stdout: + {"ok": true, "message": "Context7 MCP endpoint is reachable"} + {"ok": false, "message": "Context7 MCP endpoint is unreachable: "} + +Exit code is 0 on success, 1 on any failure. +stdlib only (urllib). +""" +import json +import sys +import urllib.error +import urllib.request + + +def _result(ok, message): + print(json.dumps({"ok": ok, "message": message})) + sys.exit(0 if ok else 1) + + +def main(): + url = "https://mcp.context7.com/mcp" + + body = json.dumps({ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": {"name": "skald-verify", "version": "1.0.0"}, + }, + }).encode("utf-8") + + req = urllib.request.Request( + url, + data=body, + headers={ + "Content-Type": "application/json", + "Accept": "application/json, text/event-stream", + }, + method="POST", + ) + + try: + with urllib.request.urlopen(req, timeout=15) as resp: + data = resp.read().decode("utf-8") + if 200 <= resp.status < 300: + # Check for a valid JSON-RPC result in the SSE event stream + if '"result"' in data and '"serverInfo"' in data: + _result(True, "Context7 MCP endpoint is reachable") + _result(False, f"Context7 returned unexpected response: {data[:200]}") + _result(False, f"Context7 returned HTTP {resp.status}") + except urllib.error.HTTPError as e: + _result(False, f"Context7 returned HTTP {e.code}: {e.reason}") + except Exception as e: + _result(False, f"Context7 MCP endpoint is unreachable: {e}") + + +if __name__ == "__main__": + main() diff --git a/docs/connector.manifest_guide.md b/docs/connector.manifest_guide.md new file mode 100644 index 0000000..0b36d9b --- /dev/null +++ b/docs/connector.manifest_guide.md @@ -0,0 +1,338 @@ +# Skald Connector Authoring Guide + +Instructions for generating a **correct connector** for the Skald marketplace +(`https://connectors.skaldagent.net`). Give this file to the agent that produces +new connectors. + +A connector is a folder served by the marketplace. Skald installs it, verifies +every file against a SHA-256 pinned in the index, then either runs it on the host +(global connector) or copies it into the user's container and runs it there +(per-user connector, blueprint §6/§7). + +--- + +## 1. The two documents + +### 1a. The root index — `connectors.json` + +One array of entries, each pointing at a connector folder. **The index is the +signable root: it is the only place that lists a connector's files and their +SHA-256 digests.** Skald refuses any file whose bytes do not match. + +```jsonc +{ + "version": 1, + "connectors": [ + { + "id": "whatsapp", // unique slug = folder name + "name": "WhatsApp", + "version": 1, // INTEGER build number — the update key (§7) + "version_string": "2.0.1", // semver, display only + "version_release_date": "2026-07-19", // ISO date, display only + "type": "mcp_local", // mcp_local | mcp_remote (see §3) + "scope": "user", // user | global (see §3) + "icon_small": "whatsapp/icon_sm.svg", + "icon_large": "whatsapp/icon_lg.svg", + "user_description": "Send and read WhatsApp messages from your linked account.", + "requires": ["NODE"], // human hint: NODE | PYTHON | OAUTH | API_KEY + "tags": ["messaging", "mcp", "local", "whatsapp", "qr"], + "auth": { "type": "qr" }, // may be repeated here and in the manifest + "tools": [ // OPTIONAL — friendly UI names per tool (§2a) + { "name": "send_message", "display_name": "Send Message" } + ], + "folder": "whatsapp", // defaults to id + "files": [ + { "path": "index.js", "sha256": "…", "size": 21258 }, + { "path": "package.json", "sha256": "…", "size": 302 }, + { "path": "connector.json", "sha256": "…", "size": 620 }, + { "path": "icon_sm.svg", "sha256": "…", "size": 306 }, + { "path": "icon_lg.svg", "sha256": "…", "size": 308 } + ] + } + ] +} +``` + +**Rules** + +- `files[].path` is relative to the connector folder. List **every** file the + connector ships (server code, `package.json`/`requirements.txt`, icons, and the + `connector.json` itself). A missing or mismatched digest fails the install. +- Compute `sha256` over the exact bytes served: `sha256sum `. +- Do **not** list `node_modules/` or any generated deps — those are installed on + the box, not shipped (see §5). +- `size` is optional but recommended. +- `tools[]` is optional but, when present in the manifest, should also be listed here + so that the index is self-contained (the index is the only document Skald downloads + eagerly — metadata UIs that need friendly tool names look here first, §2a). + Structure: `{ "name": "", "display_name": "" }`. + +### 1b. The per-connector manifest — `/connector.json` + +The richer document. Fetched per connector and mapped into Skald's catalog. + +```jsonc +{ + "id": "whatsapp", + "name": "WhatsApp", + "version": 1, // INTEGER build number — the update key (§7) + "version_string": "2.0.1", // semver, display only + "version_release_date": "2026-07-19", // ISO date, display only + "type": "mcp_local", + "scope": "user", + "auth": { "type": "qr" }, // none | api_key | oauth2 | qr (see §4) + "mcp_config": { + "command": "node", // interpreter (local) … + "args": ["index.js"], // … args[0] MUST name the entry file + "transport": "stdio" // stdio (local) | streamable-http (remote) + }, + "docs": [{ + "lang": "en", + "description": "Human blurb shown in the UI.", + "llm_short_description": "One line the model reads to decide whether to use this connector." + }], + "env": [], // form fields the user fills (see §4b) + "tools": [ // OPTIONAL — friendly UI names per tool (§2a) + { "name": "send_message", "display_name": "Send Message" } + ], + "homepage": "https://…", + "icon_small": "icon_sm.svg", // relative to the folder here + "icon_large": "icon_lg.svg", + "tags": ["messaging", "mcp", "local", "whatsapp", "qr"] +} +``` + +**`mcp_config.args[0]` is load-bearing for a local connector:** it is how Skald +learns which file to run. At activation Skald rewrites it to the file's path +inside the user's container (`/root/.skald/mcp//`), so keep it a +plain relative filename (`index.js`, `server.py`, `pkg/server.py`). + +--- + +## 2. Server contract (MCP over stdio) + +A **local** connector is a program speaking JSON-RPC 2.0 over stdin/stdout. It +MUST handle: + +- `initialize` → `{ protocolVersion, capabilities: { tools: {} }, serverInfo }` +- `notifications/initialized` → no response +- `tools/list` → `{ tools: [ { name, description, inputSchema } ] }` +- `tools/call` → `{ content: [ { type: "text", text } ], isError? }` + +**stdout is reserved for JSON-RPC only.** Send all logs/diagnostics to **stderr**. +Anything a library prints to stdout (a logger, a banner) corrupts the protocol — +silence it (e.g. Baileys/pino → a silent logger; Python → `print(…, file=sys.stderr)`). + +A **remote** connector is an HTTP MCP endpoint (`mcp_config.url` + +`transport: "streamable-http"`); no code runs on the box. + +### 2a. Friendly tool names (`tools[]`) — optional + +Raw MCP tool names are ugly in the chat UI (`search_files`, `send_message`). The +optional top-level `tools[]` block gives each one a human title shown as the tool +card's heading: + +```jsonc +"tools": [ + { "name": "send_message", "display_name": "Send Message" }, + { "name": "list_chats", "display_name": "List Chats" }, + { "name": "download_media", "display_name": "Download Media" } +] +``` + +- `name` — the **raw** tool name exactly as your server returns it from `tools/list`. +- `display_name` — the friendly card title (English only; not internationalized). + +**Resolution order** for a tool's card title is **`tools[].display_name` → the MCP +`title` field → a prettified raw name**. So you have two ways to set a friendly +name, and can skip `tools[]` entirely: + +1. **This block** — the authoritative override, curated in the manifest. +2. **The MCP `title` field** — if your `tools/list` entries already carry a + `title` (MCP 2025-06-18+), Skald uses it automatically; no manifest change + needed. `tools[]` wins if both are present. +3. If neither is set, Skald title-cases the raw name (`send_message` → "Send + Message"). + +**Icons are per connector, not per tool.** Every tool of a connector shows that +connector's own `icon_small`; there is no per-tool icon field. Only list a tool in +`tools[]` when its prettified name isn't good enough — partial lists are fine +(unlisted tools fall through to steps 2–3). + +--- + +## 3. Placement & risk vocabulary (what the words mean) + +| Manifest | Meaning | +| --- | --- | +| `scope: "user"` | runs **once per user**, inside their container. Personal creds. | +| `scope: "global"` | runs **once for the household**, on the host. Shared, stateless. Admin enables it with a key. | +| `type: "mcp_local"` | ships code that will **execute on the box** — installing needs the admin `mcp.register_local_script` capability (RCE-bearing act, §14). | +| `type: "mcp_remote"` | just an HTTP URL; no local code. | + +Pick the narrowest: a personal messaging/email/calendar connector is +`scope: "user"`; a shared search API is `scope: "global"`. + +--- + +## 4. Authentication (`auth.type`) + +| `auth.type` | Flow | Ships | +| --- | --- | --- | +| `none` | nothing to sign in | — | +| `api_key` | user pastes a key/secret into a form | an `env[]` schema (§4b) | +| `oauth2` | browser consent → paste code back | `auth.provider` + `auth.scopes` + `auth.deliver` (§4c) | +| `qr` | server shows a QR, user scans with a phone | a `login_status` tool (§4d) | + +### 4b. `api_key` — the `env[]` schema + +Each entry drives one form field **and** is injected as an env var / URL token to +the server: + +```jsonc +"env": [{ + "name": "tavilyApiKey", + "label": "Tavily API key", + "description": "Create one at https://app.tavily.com.", + "required": true, + "secret": true, // rendered masked, stored encrypted + "example": "tvly-xxxxxxxx" +}] +``` + +The server reads each value from `process.env.` (or `os.environ`). For a +**remote** connector that wants the key in the URL, use a placeholder: +`"url": "https://mcp.example.com/?key={SECRET:tavilyApiKey}"`. + +### 4c. `oauth2` — provider consent + +```jsonc +"auth": { + "type": "oauth2", + "provider": "google", // slug into the admin's sign-in providers + "scopes": ["https://www.googleapis.com/auth/gmail.modify"], + "deliver": { "as": "env", "format": "google_authorized_user", "env": "GMAIL_CREDS_JSON" } +} +``` + +The manifest names **only** the provider slug, scopes, and how the obtained token +is delivered — never client secrets or endpoint URLs (those are admin-entered, +kept off the public feed). Skald handles PKCE + code exchange and injects the +credential as the named env var. `format`: `google_authorized_user` (Google) or +`refresh_token`. Today only `as: "env"` is wired. + +### 4d. `qr` / interactive device login — the generic contract + +For a connector whose credential is produced by **scanning/pairing** (WhatsApp +today), there is no code to paste. The rule: + +> **Expose one extra tool, `login_status`, returning a JSON object** (as the +> `text` of a normal text result). Skald calls it directly (never the agent) and a +> login panel polls it. + +```jsonc +// login_status result text (a JSON string): +{ + "state": "connecting" | "need_scan" | "ready" | "logged_out", + "qr": "data:image/png;base64,…", // present ONLY while state == need_scan + "message": "human-readable line" +} +``` + +- `activate` on a `qr` connector inserts a **pending** row and **starts the + server** (so it can produce the QR), then hands off to the login panel. +- The panel polls `POST /api/mcp/login/status`; when `state == "ready"` the + connector is marked ready and starts automatically on later logins. +- Also expose a `logout` tool (clears the session, forces a fresh QR) — the panel + calls it via `POST /api/mcp/login/reset` to re-link a different phone. +- The **credential is the on-disk session**, not a token. Persist it **inside the + connector's own directory** (e.g. `./auth/` next to the entry file). That folder + lives under the bind-mounted home, so it survives container recreates and + connector updates. Never store it under a shared/global path. + +Skald resolves `auth.type: "qr"` the same way whether it appears in the index +entry or the manifest. + +--- + +## 5. Dependencies (node & python) — how they get installed + +**Do not ship `node_modules/` or vendored wheels.** Declare deps as a standard +manifest **file** and Skald installs them inside the container: + +- **node:** ship a `package.json` with a `dependencies` map. Skald runs + `npm ci --omit=dev` (falling back to `npm install --omit=dev`) in the connector + dir. `node_modules/` resolves automatically beside the entry file. +- **python:** ship a `requirements.txt`. Skald installs it with + `pip install --target .pydeps` and puts `.pydeps` on the server's `PYTHONPATH`. + +This runs at activation **and** on every startup, guarded by a **content hash** of +the connector's source files: + +- first activation / a brand-new container → full install, +- a connector **update** (any shipped file changed) → re-copy + re-install, +- unchanged → skipped in microseconds. + +So you never write install steps into the manifest — just ship the dep file, list +it in the index with its SHA-256, and set `requires: ["NODE"]` / `["PYTHON"]` as a +human hint. Pin versions in `package.json` / `requirements.txt` for reproducible +installs. Keep the dep tree lean (containers are slim; avoid native-heavy +packages where a pure alternative exists — e.g. Baileys instead of a browser). + +--- + +## 6. Verify-before-save (optional but recommended) + +Ship a `verify.py` / verify snippet and reference it: + +```jsonc +"verify": { "command": "python3 verify.py", "timeout_secs": 15 } +``` + +It runs with the collected env/secret injected and must print **one JSON object** +on stdout: `{"ok": bool, "message": string, "details"?: object}`, exit 0 on +success. Used for `api_key`/`none` connectors to test creds before activating. +(A `qr` connector needs no verify — its `login_status` is the live check.) + +--- + +## 7. Versioning & updates + +Three fields, in **both** the index entry and the `connector.json`, kept identical: + +| field | type | role | +| --- | --- | --- | +| `version` | **integer** | monotonic build number, **per connector** — the machine comparison key | +| `version_string` | string (semver) | display only | +| `version_release_date` | ISO date `YYYY-MM-DD` | display only | + +- `version` is a **number, not a string** (`1`, not `"1"` or `"2.0.1"`). Start at + `1` for the first release under this scheme; **`+1` on every change** to any + shipped file. Never reuse or decrement. +- Skald stores the installed `version` and compares it to the feed's: a strictly + greater feed `version` shows **"update available"** in the marketplace, and the + Install button becomes **Update**. Clicking it re-downloads the files and users + pick up the new code + deps on their next login. +- **The integer is the UI "is there an update?" signal — the actual re-install + trigger is the content-hash (§5).** So an update still propagates even if the + number is not bumped; but always bump it, or the admin never sees the update. +- `version_string` and `version_release_date` are display metadata only — never + compared. (Migration note: replace any legacy string `"version": "2.0.1"` with + the integer `version` + `version_string`.) + +--- + +## 8. Checklist for a new connector + +1. Folder `myconn/` with: entry file, `connector.json`, deps file + (`package.json`/`requirements.txt`), `icon_sm.svg`, `icon_lg.svg`, + optional `verify.*`. +2. Server speaks MCP over stdio (§2); **stdout = JSON-RPC only**. +3. `mcp_config.args[0]` names the entry file. +4. Correct `type` + `scope` (§3) and `auth.type` (§4). +5. For `qr`: implement `login_status` (+ `logout`), persist the session under the + connector dir (§4d). +6. Deps declared as a file, **not** vendored (§5). +7. Add the entry to `connectors.json` with a correct `sha256` for **every** file. +8. Bump `version`. +```