Aggiunto connector Context7 (mcp_remote)
- URL: https://mcp.context7.com/mcp, trasporto streamable-http - Auth: none (endpoint pubblico, free tier) - Verify: MCP initialize probe (testato ✅) - Icone SVG dal logo ufficiale Context7 Inoltre: - Copiata docs/connector.manifest_guide.md da skald-circle/blueprint/ - Riferimento alla guida in SKALD.md
This commit is contained in:
@@ -1,6 +1,10 @@
|
|||||||
# Skald Connectors Marketplace
|
# 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`
|
**Remote**: `https://git.skaldagent.net/dguiducci/skald-connectors.git`
|
||||||
**Live**: `https://connectors.skaldagent.net/`
|
**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 |
|
| 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) |
|
| `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`) |
|
| `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) |
|
| `serpapi-flights` | SerpAPI Flights | `mcp_remote` | `global` | api_key (`{SECRET:serpapiApiKey}` in URL) | `verify.py` (MCP initialize probe) |
|
||||||
|
|||||||
@@ -838,6 +838,61 @@
|
|||||||
"size": 285
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<svg width="96" height="96" viewBox="0 0 96 96" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="4" y="4" width="88" height="88" rx="12" fill="black"/>
|
||||||
|
<path d="M31.144 48.512c0 4.492-1.822 8.244-4.788 11.698h6.908v3.348H23.38v-3.18c3.242-3.664 4.492-6.652 4.492-11.866h3.272z" fill="white"/>
|
||||||
|
<path d="M46.856 48.512c0 4.492 1.822 8.244 4.788 11.698h-6.908v3.348h10.596v-3.18c-3.242-3.664-4.492-6.652-4.492-11.866h-3.984z" fill="white"/>
|
||||||
|
<path d="M31.144 43.488c0-4.492-1.822-8.242-4.788-11.696h6.908V28.444H23.38v3.18c3.242 3.664 4.492 6.652 4.492 11.864h3.272z" fill="white"/>
|
||||||
|
<path d="M46.856 43.488c0-4.492 1.822-8.242 4.788-11.696h-6.908V28.444h10.596v3.18c-3.242 3.664-4.492 6.652-4.492 11.864h-3.984z" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 739 B |
@@ -0,0 +1,7 @@
|
|||||||
|
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="2" y="2" width="44" height="44" rx="6" fill="black"/>
|
||||||
|
<path d="M16.572 24.256c0 2.246-.911 4.122-2.394 5.849h3.454v1.674h-5.298v-1.59c1.621-1.832 2.246-3.326 2.246-5.933h1.992z" fill="white"/>
|
||||||
|
<path d="M23.428 24.256c0 2.246.911 4.122 2.394 5.849h-3.454v1.674h5.298v-1.59c-1.621-1.832-2.246-3.326-2.246-5.933h-1.992z" fill="white"/>
|
||||||
|
<path d="M16.572 21.744c0-2.246-.911-4.121-2.394-5.848h3.454v-1.674h-5.298v1.59c1.621 1.832 2.246 3.326 2.246 5.932h1.992z" fill="white"/>
|
||||||
|
<path d="M23.428 21.744c0-2.246.911-4.121 2.394-5.848h-3.454v-1.674h5.298v1.59c-1.621 1.832-2.246 3.326-2.246 5.932h-1.992z" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 724 B |
@@ -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: <reason>"}
|
||||||
|
|
||||||
|
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()
|
||||||
@@ -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 <file>`.
|
||||||
|
- 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": "<raw>", "display_name": "<friendly>" }`.
|
||||||
|
|
||||||
|
### 1b. The per-connector manifest — `<folder>/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/<name>/<entry>`), 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.<name>` (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`.
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user