add playwright connector (v1 / 1.0.0) + commit pending docs reorganization
- 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
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to the Skald Connectors Marketplace are documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## 2026-08-19
|
||||
|
||||
### Added
|
||||
|
||||
- New connector: **Playwright** (mcp_local, scope global, v1 / 1.0.0) — full browser automation via the official `@playwright/mcp@0.0.79` (Microsoft), wrapped like http-fetch/firecrawl: `package.json` pins the package and `index.js` rewrites argv (`--headless --isolated --no-sandbox`, plus a `process.argv.slice(2)` passthrough) and imports the package's `cli.js`, which self-executes at import time. `cli.js` is not in the package `exports` map, so the wrapper resolves it from the exported `package.json` path.
|
||||
- `auth: none`, `requires: ["NODE"]`. Runs headless with an **isolated in-memory profile**: no cookies/login state persisted between sessions (chosen over the default persistent profile — a shared household browser must not accumulate per-user sessions, and a persistent profile also allows only one browser instance at a time).
|
||||
- **Browser download**: the upstream `playwright` package does NOT download browsers at `npm install` (verified on 1.63.0-alpha-2026-08-05), and `@playwright/mcp` 0.0.79 no longer ships the old `browser_install` tool (the README section is now empty). So the connector's `package.json` carries a `postinstall` hook — `node node_modules/@playwright/mcp/cli.js install-browser chromium` — which downloads Chromium + headless shell + ffmpeg only (~350 MB; a bare `install-browser` would also fetch Firefox and WebKit). Verified on a cold directory: `npm install --omit=dev` runs the hook and the browser launches.
|
||||
- `verify.js` (30s) resolves `playwright-core` relative to `@playwright/mcp` (it is a transitive dep, not declared by the connector's own package.json) and launches a real headless Chromium on about:blank — catches the two real failure modes (binary missing, system libraries missing on slim hosts) before the activation is saved.
|
||||
- Default tool set = 24 tools (no `--caps` extras); all get `display_name` in `tools[]` because `@playwright/mcp` does not emit MCP `title` fields in `tools/list` (the "Title:" lines in its README are not on the wire).
|
||||
- ⚠️ Security: the default set includes `browser_run_code_unsafe` (RCE-equivalent, arbitrary JS in the server process) and `browser_evaluate` (arbitrary JS in the page); there is no CLI flag to disable individual core tools. Consistent with the `mcp_local` trust model (guide §3) and declared in the manifest descriptions.
|
||||
- Icons: official Playwright SVG (Microsoft catalog), sized 48/96.
|
||||
- Tested E2E reproducing skald's path (`npm install --omit=dev` + `node index.js`): `initialize`, `tools/list` (24 tools), real `browser_navigate` + `browser_snapshot` on https://example.com ✅, verify probe ok ✅
|
||||
|
||||
## 2026-08-10
|
||||
|
||||
### Fixed
|
||||
|
||||
- **http-fetch + firecrawl: `npx` launch was broken (v5 / 1.1.0)** — both connectors declared `mcp_config: {command: "npx", args: ["-y", "<package>"]}` and shipped no code files (only `connector.json` + icons).
|
||||
- `npx -y <package>` is not expressible in skald. For a `type: mcp_local`, skald treats `args[0]` as the **name of the file to run**, not as an argument: at install it computes `script_path = "<id>/" + args[0]` and clears `args_json` (`marketplace.rs::install`), then `global_enable` (`api/mcp.rs`) resolves it to an absolute path and launches `<command> <abs>`. The real command became `npx /…/connectors/http-fetch/-y` — a nonexistent path, with `-y` and the package name lost. The process never answered `initialize`, so `start_server` failed.
|
||||
- The failure was silent: `global_enable` still returns HTTP 200 with an `error` field in the body, so the UI showed the connector as enabled while the runtime had no server. And `render_mcp_list` (`loop_adapters/system.rs`) builds the `## MCP servers` table from `mcp.tools()`, i.e. the **live runtime state**, not the DB → the connector appeared activated and granted to the user but **absent from the system context**. ⚠️ This combination (200 + `error` in the body) makes any connector that fails to start invisible in the UI: worth surfacing on the skald side.
|
||||
- Fix — two-file wrapper for both: a `package.json` pinning the upstream package (`mcp-fetch-server@1.1.2`, `firecrawl-mcp@3.23.7`) and an `index.js` that imports it for side effects (the module starts the JSON-RPC loop on stdio at import). `mcp_config` becomes `{command: "node", args: ["index.js"], transport: "stdio"}`, i.e. a real `local_script`: `ensure_installed_host` runs `npm ci --omit=dev || npm install --omit=dev` in the connector folder before launch, exactly like whatsapp. No `node_modules` shipped, no lockfile (like whatsapp).
|
||||
- Removed legacy fields `launch_command`, top-level `transport`, and `dependencies` (`dependencies` is only for the card, as already seen on gmaps; `transport` belongs inside `mcp_config`).
|
||||
- firecrawl: removed `mcp_config.env: {"FIRECRAWL_API_KEY": "{SECRET:FIRECRAWL_API_KEY}"}` — inert, same case as gmaps on 2026-08-10: `apply_key_placeholder` substitutes tokens only in the URL, never in `env` values. It worked because the admin form sends `env` and that payload overwrites `entry.env_json`.
|
||||
- firecrawl: added `firecrawl_developer_search` to `tools[]` (27 live tools vs 26 declared, verified on 3.23.7); `requires` `["NODE"]` → `["NODE", "API_KEY"]`.
|
||||
- Re-aligned manifest↔fragment versions to `5` / `1.1.0` / `2026-08-10` for both: they were 2/1.0.1 (manifest) vs 4/1.0.3 (fragment), and skald prefers the manifest — so `installed_version` stayed at 2 and the "Update available" badge would never have appeared.
|
||||
- Host requirement: these are `scope: global` connectors, they run on the **host**, not in the container. `mcp-fetch-server` wants Node ≥18, `firecrawl-mcp` wants Node ≥**22**.
|
||||
- Tested end-to-end reproducing skald's path (`npm ci || npm install` + `node <abs>/index.js`): `initialize`, `tools/list`, and a real `tools/call`, stdout only JSON-RPC, clean stderr ✅
|
||||
- Index regenerated with compile.py ✅
|
||||
- ⚠️ **Applying this to an instance that already has the connector installed is not just an Update.** `refresh_connector_after_reinstall` (`skald/accessors.rs`) updates only the `description` of the `mcp_global_servers` row, then restarts from that row: `command` and `args_json` stay the ones snapshotted at the first `global_enable`, i.e. still `npx` + `/…/connectors/<id>/-y`. Procedure: deploy → **Update** from the marketplace (rewrites `script_path` in the catalog) → open the connector page and **re-save the config**, the only call that recomputes `command`/`args` and runs `ensure_installed_host`. Same care already noted for gmaps. Whether the refresh should also re-derive command/args is open for evaluation on the skald side.
|
||||
|
||||
- **gmaps: missing dependency install + verify wired (v6 / 1.1.0)**
|
||||
- Added `requirements.txt` (`googlemaps>=4.10.0`) — it was the only python connector without one. Dependencies were declared in the manifest `dependencies` field, which skald uses **only for the card**: `ensure_installed_host` looks exclusively at `requirements.txt` / `package.json`. Result: empty `.pydeps` and logs full of `No module named 'googlemaps'`, with the server still answering `tools/list` (→ `connected — 6 tool(s)` on a broken connector).
|
||||
- Wired the `verify` (`python3 verify.py`, 20s): `verify.py` was shipped but the manifest had no `verify` block, and skald reads `verify_command` only from there. Now an activation with broken dependencies fails visibly instead of starting silently.
|
||||
- `verify.py` puts `.pydeps` on `sys.path`: skald sets `PYTHONPATH` only for the *server* process (`global_row_spec`), while verify runs as `sh -c "python3 verify.py"` without it. Without this line, verify would fail with "Missing dependency" even on a correctly installed connector, disabling the row. ⚠️ **Same latent risk for every connector with a `verify` that imports non-stdlib dependencies** (gcal in container): to be checked.
|
||||
- Removed `mcp_config.env: {"GOOGLE_MAPS_API_KEY": "{SECRET:…}"}` (introduced 2026-07-23): inert. `apply_key_placeholder` substitutes `{SECRET:}`/`{ENV:}` tokens **only in the URL**, never in `env` values. It worked because the admin form sends `env` and that payload overwrites `entry.env_json`; with an empty form the process would have received the literal string. ⚠️ **The spec in this file and in CLAUDE.md says the opposite** — either fix the spec, or extend the substitution to `env` values on the skald side.
|
||||
- Re-aligned manifest↔fragment versions to `6` / `1.1.0` / `2026-08-10`: they were 2/1.0.1 vs 5/1.0.4, and skald prefers the manifest (`manifest.version.or(entry.version)`) — so `installed_version` stayed at 2 and the "Update available" badge would never have appeared.
|
||||
- `requires`: `ENV` → `API_KEY`; server error messages cleaned of references to `secrets/gmaps_api_key.txt` (deprecated path).
|
||||
- Index regenerated with compile.py ✅
|
||||
|
||||
## 2026-08-07
|
||||
|
||||
### Added
|
||||
|
||||
- New connector: **LinkedIn** (mcp_local, scope user): server.py + session.py + verify.py + PNG icons
|
||||
- Added to `connectors/index.json`, index regenerated with compile.py (17 connectors total)
|
||||
- `.gitignore` updated to ignore `.claude/`
|
||||
- Deploy to connectors.skaldagent.net via `skaldserver` alias (192.168.1.100, LAN — no Tailscale)
|
||||
|
||||
## 2026-07-23
|
||||
|
||||
### Changed
|
||||
|
||||
- **gmaps: env var injection fix** — added `mcp_config.env` in `connector.json` to inject `GOOGLE_MAPS_API_KEY` into the MCP process. The connector was declared as `delivery: env` but without `mcp_config.env` Skald could not pass the variable to the Python process. Version bump: fragment 4→5, connector 1→2. Index regenerated with compile.py ✅
|
||||
- **Context7 icon update (PNG)** — replaced Context7 icons from SVG to PNG (new icon provided by the user): `icon_sm.png` — 48×48 (2.7 KB), `icon_lg.png` — 96×96 (4.5 KB). Old SVGs removed, references updated in fragment.json and connector.json. Version bump: fragment 3→4, connector 1→2. Index regenerated with compile.py ✅
|
||||
- **SerpAPI Flights icon update (PNG)** — replaced SerpAPI Flights icons from SVG to PNG: `icon_sm.png` — 48×48 (2.9 KB), `icon_lg.png` — 96×96 (6.8 KB). Old SVGs removed, references updated in fragment.json and connector.json. Version bump: fragment 4→5, connector 2→3. Index regenerated with compile.py ✅
|
||||
@@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
The **Skald Connectors Marketplace** — a catalog of tested connectors (adapters that let Skald agents talk to external services). 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 Python MCP scripts that are served as-is over HTTP.
|
||||
|
||||
`SKALD.md` is the authoritative spec (written in Italian) — read it before changing any schema or manifest.
|
||||
`SKALD.md` is the authoritative spec — read it before changing any schema or manifest.
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -25,15 +25,15 @@ Connector taxonomy (see SKALD.md for full enums):
|
||||
- `{ENV:NAME}` — a non-sensitive value (host, port, username…), declared in the top-level `env[]` array.
|
||||
- `{SECRET:NAME}` — a sensitive value (password, API key, token), also declared in `env[]` with `secret: true`.
|
||||
|
||||
Legacy tokens `{key}`, `{env:NAME}`, `{secrets}/…` are **deprecated** and skald no longer substitutes them. The api-key of an `auth.type = "api_key"` connector is exposed as `{SECRET:<auth.param>}` (e.g. Tavily: `?tavilyApiKey={SECRET:tavilyApiKey}`). See SKALD.md § Sintassi placeholder.
|
||||
Legacy tokens `{key}`, `{env:NAME}`, `{secrets}/…` are **deprecated** and skald no longer substitutes them. The api-key of an `auth.type = "api_key"` connector is exposed as `{SECRET:<auth.param>}` (e.g. Tavily: `?tavilyApiKey={SECRET:tavilyApiKey}`). See SKALD.md § Placeholder syntax.
|
||||
|
||||
**Config via `env[]` (`env` manifest field):** a connector whose `requires` includes `ENV` declares its required environment variables in a top-level `env` array in `connector.json` — each entry has `name`, `label`, `description`, `required`, `secret`, and optional `default`/`example`. skald renders a real form from this schema (masking `secret: true` fields, marking `required`, using `example` as placeholder) and injects the collected values into the process environment at launch; the server reads `os.environ`. Nothing is written to disk. The `email` connector is the reference example (see SKALD.md § Campo env). **`tavily` now also declares `env[]`** (its API key as a `secret` field).
|
||||
**Config via `env[]` (`env` manifest field):** a connector whose `requires` includes `ENV` declares its required environment variables in a top-level `env` array in `connector.json` — each entry has `name`, `label`, `description`, `required`, `secret`, and optional `default`/`example`. skald renders a real form from this schema (masking `secret: true` fields, marking `required`, using `example` as placeholder) and injects the collected values into the process environment at launch; the server reads `os.environ`. Nothing is written to disk. The `email` connector is the reference example (see SKALD.md § The env field). **`tavily` now also declares `env[]`** (its API key as a `secret` field).
|
||||
|
||||
**Verify-before-save (`verify` manifest field).** A connector may declare a `verify` step — a shell command skald runs *after* the user fills the form and *before* persisting the activation, to confirm the credentials actually work:
|
||||
```json
|
||||
"verify": { "command": "python3 verify.py", "timeout_secs": 20 }
|
||||
```
|
||||
The command runs in the same sandbox as the server (container `skald-{userid}` for `mcp_local` user, host for `mcp_remote` global), with the collected env/secret injected. It must print one JSON object on stdout — `{"ok": bool, "message": string, "details"?: object}` — and exit 0 on success. If `verify` is absent, no test runs and the activation is direct. See SKALD.md § Campo verify. `email` and `tavily` ship a `verify.py`; `gmail` will get one in the OAuth phase.
|
||||
The command runs in the same sandbox as the server (container `skald-{userid}` for `mcp_local` user, host for `mcp_remote` global), with the collected env/secret injected. It must print one JSON object on stdout — `{"ok": bool, "message": string, "details"?: object}` — and exit 0 on success. If `verify` is absent, no test runs and the activation is direct. See SKALD.md § The verify field. `email` and `tavily` ship a `verify.py`; `gmail` will get one in the OAuth phase.
|
||||
|
||||
**MCP servers** (`connectors/gmail/gmail_mcp_server.py`, `connectors/email/email_mcp_server.py`) are hand-rolled JSON-RPC 2.0 servers over stdio — no MCP SDK / FastMCP. Shared conventions: **stdout is reserved for JSON-RPC**, all logging goes to stderr, a lock guards stdout writes, a background thread emits `event/new_email` push notifications, and each server exposes the same tool shape (`TOOLS` manifest + `TOOL_DISPATCH` + `handle_request`). Mirror this structure for new local connectors.
|
||||
- **Gmail** uses the Gmail API with OAuth; today it still reads its token from `./secrets/gmail_creds.json` (override `GMAIL_CREDS_PATH`) generated by `gmail_oauth_setup.py`. The `secrets/` path is **deprecated** — the OAuth phase (skald Fase 2) will migrate Gmail to `{ENV:}`/`{SECRET:}` and a loopback-listener flow. Push = History API polling. `verify` is not yet wired for Gmail.
|
||||
@@ -48,6 +48,15 @@ The command runs in the same sandbox as the server (container `skald-{userid}` f
|
||||
- **A new `verify.py` must be added to `files[]`** (with `sha256`+`size`) exactly like any other shipped file — skald refuses to run an unverified script.
|
||||
- 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.
|
||||
|
||||
## 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](https://keepachangelog.com/en/1.1.0/) format (version headings, `Added`/`Changed`/`Fixed`/`Removed` categories, `[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.
|
||||
- Never put changelog entries in `SKALD.md`; it is spec-only.
|
||||
|
||||
## Commands
|
||||
|
||||
Recompute a file hash (do this before deploy for every changed file; SKALD.md mentions a `scripts/update_hashes.py` helper that does not exist in the repo yet):
|
||||
|
||||
@@ -0,0 +1,348 @@
|
||||
# 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
|
||||
"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.
|
||||
|
||||
### 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 **or to any manifest metadata** (description, icons, `version_string`).
|
||||
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 rewrites
|
||||
the catalog row.
|
||||
- **The integer is the *only* "is there an update?" signal** — it is compared
|
||||
strictly (`feed > installed`). `version_string` (semver), icons and
|
||||
`llm_short_description` are **never** compared, so a change to any of them that
|
||||
does not also bump the integer is **invisible**: no "update available" badge
|
||||
appears. This is the common trap — a "content-only" edit (e.g. a better
|
||||
`llm_short_description`) that forgets the integer.
|
||||
- **Two propagation paths, do not conflate them:**
|
||||
- *Per-user code + deps* (the scripts, `package.json`/`requirements.txt`) reconcile
|
||||
on a **content-hash** of the source files (§5), so new code lands at each user's
|
||||
next login even without a reinstall.
|
||||
- *Catalog metadata* (`llm_short_description` → the model's prompt, icons, friendly
|
||||
name) is **not** in that hash — it lives in the catalog row and is rewritten only
|
||||
by an explicit **reinstall/Update**. On reinstall Skald re-pulls the current feed
|
||||
(never the browse cache) and pushes the new description live: enabled global
|
||||
servers restart with it, and every logged-in user who activated the connector has
|
||||
it restarted with the fresh `llm_short_description` — no re-login needed.
|
||||
- So: to ship a new `llm_short_description`, **bump the integer** (so the admin sees
|
||||
"update available") and the admin clicks **Update**. Nothing auto-propagates a
|
||||
description change.
|
||||
- `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`.
|
||||
```
|
||||
@@ -1,64 +1,17 @@
|
||||
# Skald Connectors Marketplace
|
||||
|
||||
## What it is
|
||||
|
||||
### 2026-08-10 — http-fetch + firecrawl: fix `npx` (v5 / 1.1.0)
|
||||
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.).
|
||||
|
||||
Entrambi i connector erano **non funzionanti**: dichiaravano `mcp_config: {command: "npx", args: ["-y", "<package>"]}` e non spedivano nessun file di codice (solo `connector.json` + icone).
|
||||
Two types of connectors:
|
||||
|
||||
- **`npx -y <package>` non è esprimibile in skald.** Per un `type: mcp_local` skald tratta `args[0]` come il **nome del file da eseguire**, non come un argomento: all'install calcola `script_path = "<id>/" + args[0]` e azzera `args_json` (`marketplace.rs::install`), poi `global_enable` (`api/mcp.rs`) lo risolve in path assoluto e lancia `<command> <abs>`. Il comando reale diventava `npx /…/connectors/http-fetch/-y` — un path inesistente, con `-y` e il nome del package spariti. Il processo non rispondeva mai a `initialize`, `start_server` falliva.
|
||||
- **Il fallimento era silenzioso**: `global_enable` restituisce comunque HTTP 200 con un campo `error` nel body, quindi la UI mostrava il connector come abilitato mentre il runtime non aveva nessun server. E `render_mcp_list` (`loop_adapters/system.rs`) costruisce la tabella `## MCP servers` da `mcp.tools()`, cioè lo **stato vivo del runtime**, non dal DB → il connector risultava attivato e concesso all'utente ma **assente dal system context**. ⚠️ Questa combinazione (200 + `error` nel body) rende invisibile in UI qualsiasi connector che non parte: vale la pena farla emergere lato skald.
|
||||
- **Fix — wrapper di due file** per entrambi: `package.json` che pinna il package upstream (`mcp-fetch-server@1.1.2`, `firecrawl-mcp@3.23.7`) e `index.js` che lo importa per side-effect (il modulo avvia il loop JSON-RPC su stdio all'import). `mcp_config` diventa `{command: "node", args: ["index.js"], transport: "stdio"}`, cioè un vero `local_script`: `ensure_installed_host` fa `npm ci --omit=dev || npm install --omit=dev` nella cartella del connector prima del lancio, esattamente come per whatsapp. Nessun `node_modules` spedito, nessun lockfile (come whatsapp).
|
||||
- **Rimossi i campi legacy** `launch_command`, `transport` top-level e `dependencies` (`dependencies` è solo per la card, come già visto su gmaps; `transport` va dentro `mcp_config`).
|
||||
- **firecrawl: rimosso `mcp_config.env: {"FIRECRAWL_API_KEY": "{SECRET:FIRECRAWL_API_KEY}"}`** — inerte, stesso caso di gmaps del 2026-08-10: `apply_key_placeholder` sostituisce i token solo nella URL, mai nei valori di `env`. Funziona perché la form admin manda `env` e quel payload sovrascrive `entry.env_json`.
|
||||
- **firecrawl**: aggiunto `firecrawl_developer_search` a `tools[]` (27 tool live contro i 26 dichiarati, verificato su 3.23.7); `requires` `["NODE"]` → `["NODE", "API_KEY"]`.
|
||||
- **Versioni riallineate** manifest↔fragment a `5` / `1.1.0` / `2026-08-10` per entrambi: erano 2/1.0.1 (manifest) vs 4/1.0.3 (fragment), e skald preferisce il manifest — quindi `installed_version` restava 2 e il badge "Update available" non sarebbe mai comparso.
|
||||
- **Requisito host**: sono connector `scope: global`, girano sull'**host** e non nel container. `mcp-fetch-server` vuole Node ≥18, `firecrawl-mcp` vuole Node ≥**22**.
|
||||
- Testati end-to-end riproducendo il path di skald (`npm ci || npm install` + `node <abs>/index.js`): `initialize`, `tools/list` e una `tools/call` reale, stdout solo JSON-RPC, stderr pulito ✅
|
||||
- Indice rigenerato con compile.py ✅
|
||||
- **`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).
|
||||
|
||||
⚠️ **Per applicarlo su un'istanza che ha già il connector installato non basta l'Update.** `refresh_connector_after_reinstall` (`skald/accessors.rs`) aggiorna della riga `mcp_global_servers` **solo la `description`**, poi riparte da quella riga: `command` e `args_json` restano quelli snapshottati al primo `global_enable`, cioè ancora `npx` + `/…/connectors/<id>/-y`. Procedura: deploy → **Update** dal marketplace (riscrive `script_path` nel catalogo) → aprire la pagina del connector e **ri-salvare la config**, che è l'unica chiamata che ricalcola `command`/`args` e lancia `ensure_installed_host`. Stessa cura già annotata per gmaps. Da valutare lato skald se il refresh debba ri-derivare anche command/args.
|
||||
## Local references
|
||||
|
||||
### 2026-08-10 — gmaps: fix dipendenze + verify (v6 / 1.1.0)
|
||||
- **Aggiunto `requirements.txt` (`googlemaps>=4.10.0`)** — era l'unico connector python senza. Le dipendenze erano dichiarate nel campo `dependencies` del manifest, che skald usa **solo per la card**: `ensure_installed_host` guarda esclusivamente `requirements.txt` / `package.json`. Risultato: `.pydeps` vuoto e log pieno di `No module named 'googlemaps'`, con il server che rispondeva comunque a `tools/list` (→ `connected — 6 tool(s)` su un connector non funzionante).
|
||||
- **Cablato il `verify`** (`python3 verify.py`, 20s): `verify.py` era shippato ma il manifest non aveva il blocco `verify`, e skald legge `verify_command` solo da lì. Ora un'abilitazione con dipendenze rotte fallisce visibilmente invece di partire in silenzio.
|
||||
- **`verify.py` si mette `.pydeps` su `sys.path`**: skald imposta `PYTHONPATH` solo per il processo *server* (`global_row_spec`), mentre il verify gira come `sh -c "python3 verify.py"` senza. Senza questa riga il verify fallirebbe con "Missing dependency" anche su un connector installato correttamente, disabilitando la riga. ⚠️ **Stesso rischio latente per ogni connector con `verify` che importa dipendenze non-stdlib** (gcal in container): da controllare.
|
||||
- **Rimosso `mcp_config.env: {"GOOGLE_MAPS_API_KEY": "{SECRET:…}"}`** (introdotto il 2026-07-23): inerte. `apply_key_placeholder` sostituisce i token `{SECRET:}`/`{ENV:}` **solo nella URL**, mai nei valori di `env`. Funzionava perché la form admin manda `env` e quel payload sovrascrive `entry.env_json`; a form vuota il processo avrebbe ricevuto la stringa letterale. ⚠️ **La spec in questo file e in CLAUDE.md dice il contrario** — o si corregge la spec, o si estende la sostituzione ai valori di `env` lato skald.
|
||||
- **Versioni riallineate** manifest↔fragment a `6` / `1.1.0` / `2026-08-10`: erano 2/1.0.1 vs 5/1.0.4, e skald preferisce il manifest (`manifest.version.or(entry.version)`) — quindi `installed_version` restava 2 e il badge "Update available" non sarebbe mai comparso.
|
||||
- `requires`: `ENV` → `API_KEY`; messaggi d'errore del server ripuliti dai riferimenti a `secrets/gmaps_api_key.txt` (path deprecato).
|
||||
- Indice rigenerato con compile.py ✅
|
||||
|
||||
### 2026-08-07 — Nuovo connector: LinkedIn
|
||||
- Aggiunto connector `linkedin` (mcp_local, scope user): server.py + session.py + verify.py + icone PNG
|
||||
- Aggiunto a `connectors/index.json`, indice rigenerato con compile.py (17 connector totali)
|
||||
- `.gitignore` aggiornato per ignorare `.claude/`
|
||||
- Deploy su connectors.skaldagent.net via alias `skaldserver` (192.168.1.100, LAN — niente Tailscale)
|
||||
|
||||
### 2026-07-23 — gmaps: fix env var injection
|
||||
- Aggiunto `mcp_config.env` in `connector.json` per iniettare `GOOGLE_MAPS_API_KEY` nel processo MCP
|
||||
- Il connector era dichiarato come `delivery: env` ma senza `mcp_config.env` Skald non sapeva passare la variabile al processo Python
|
||||
- Version bump: fragment 4→5, connector 1→2
|
||||
- Indice rigenerato con compile.py ✅
|
||||
|
||||
### 2026-07-23 — Context7 icon update (PNG)
|
||||
- Sostituite icone Context7 da SVG a PNG (icona nuova fornita dall'utente):
|
||||
- `icon_sm.png` — 48×48 (2.7 KB)
|
||||
- `icon_lg.png` — 96×96 (4.5 KB)
|
||||
- Vecchi SVG rimossi, riferimenti aggiornati in fragment.json e connector.json
|
||||
- Version bump: fragment 3→4, connector 1→2
|
||||
- Indice rigenerato con compile.py ✅
|
||||
## Reference locale
|
||||
|
||||
### 2026-07-23 — SerpAPI Flights icon update (PNG)
|
||||
- Sostituite icone SerpAPI Flights da SVG a PNG:
|
||||
- `icon_sm.png` — 48×48 (2.9 KB)
|
||||
- `icon_lg.png` — 96×96 (6.8 KB)
|
||||
- Vecchi SVG rimossi, riferimenti aggiornati in fragment.json e connector.json
|
||||
- Version bump: fragment 4→5, connector 2→3
|
||||
- Indice rigenerato con compile.py ✅
|
||||
|
||||
- **[docs/connector.manifest_guide.md](docs/connector.manifest_guide.md)** — Guida ufficiale per generare connector corretti (copiata da `skald-circle/blueprint/`)
|
||||
|
||||
_Updated: 2026-07-23_
|
||||
- **[docs/connector.manifest_guide.md](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/`
|
||||
@@ -66,75 +19,65 @@ _Updated: 2026-07-23_
|
||||
|
||||
## Deploy
|
||||
|
||||
Il branch **`main`** è il branch di **release**. Solo codice pronto per produzione finisce qui.
|
||||
Sviluppo e versioni alfa-staranno su branch separati in futuro.
|
||||
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.
|
||||
|
||||
Deploy rapido con MCP SSH:
|
||||
Quick deploy with MCP SSH:
|
||||
|
||||
```bash
|
||||
mcp__ssh__exec alias=skald-home-server command="/home/dguiducci/marketplace_deploy.sh"
|
||||
```
|
||||
|
||||
Oppure via SSH classico:
|
||||
Or via classic SSH:
|
||||
|
||||
```bash
|
||||
ssh dguiducci@skald-home-server /home/dguiducci/marketplace_deploy.sh
|
||||
```
|
||||
|
||||
Lo script sul server fa:
|
||||
The script on the server does:
|
||||
1. `git pull` in `/home/dguiducci/repos/skald-connectors/`
|
||||
2. `cp -r connectors/*` in `/var/www/connectors.skaldagent.net/`
|
||||
2. `cp -r connectors/*` into `/var/www/connectors.skaldagent.net/`
|
||||
|
||||
La directory `/var/www/connectors.skaldagent.net/` è di proprietà di `dguiducci`,
|
||||
quindi non serve sudo per la copia.
|
||||
The `/var/www/connectors.skaldagent.net/` directory is owned by `dguiducci`,
|
||||
so no sudo is needed for the copy.
|
||||
|
||||
**Prima del deploy**, ricordarsi di rigenerare l'indice:
|
||||
**Before deploying**, remember to regenerate the index:
|
||||
```bash
|
||||
python3 scripts/compile.py
|
||||
```
|
||||
|
||||
## Cos'è
|
||||
|
||||
Il **marketplace** è il catalogo dei connector testati per Skald. Ogni connector è un adattatore che permette a Skald di interfacciarsi con un servizio esterno (API, email, calendario, ricerca, messaggistica, ecc.).
|
||||
|
||||
Due tipi di connector:
|
||||
|
||||
- **`mcp_remote`** — un MCP server già hosted, accessibile via URL (es. Tavily).
|
||||
- **`mcp_local`** — script Python/Node da eseguire lato client (es. Gmail, Google Calendar).
|
||||
|
||||
## Friendly tool names (2026-07-21)
|
||||
|
||||
Ogni tool MCP deve esporre un nome friendly per la UI di Skald. Due modi, in ordine di preferenza:
|
||||
Every MCP tool must expose a friendly name for the Skald UI. Two ways, in order of preference:
|
||||
|
||||
1. **Via script MCP (preferito)** — aggiungere `"title": "Friendly Name"` nella definizione di ogni tool dentro `tools/list`. Funziona per tutti gli script locali (Python/Node) che controlliamo.
|
||||
2. **Via manifest (fallback)** — aggiungere `"tools": [{"name": "...", "display_name": "..."}]` in `connector.json` **e** in `connectors.json`. Usato solo per connector remoti o package esterni (es. `npx -y firecrawl-mcp`).
|
||||
1. **Via the MCP script (preferred)** — add `"title": "Friendly Name"` in the definition of each tool inside `tools/list`. Works for all local scripts (Python/Node) that we control.
|
||||
2. **Via the manifest (fallback)** — add `"tools": [{"name": "...", "display_name": "..."}]` in `connector.json` **and** in `connectors.json`. Used only for remote connectors or external packages (e.g. `npx -y firecrawl-mcp`).
|
||||
|
||||
**Ordine di risoluzione** (Skald li prova in quest'ordine):
|
||||
1. `tools[].display_name` dal manifest
|
||||
2. `title` dal `tools/list` dell'MCP server
|
||||
3. Prettify automatico del nome raw (`send_message` → "Send Message")
|
||||
**Resolution order** (Skald tries them in this order):
|
||||
1. `tools[].display_name` from the manifest
|
||||
2. `title` from the MCP server's `tools/list`
|
||||
3. Automatic prettify of the raw name (`send_message` → "Send Message")
|
||||
|
||||
Stato attuale (2026-07-21): tutti i 13 connector del marketplace hanno `title` nello script o `tools[]` nel manifest.
|
||||
Current status (2026-07-21): all 13 marketplace connectors have `title` in the script or `tools[]` in the manifest.
|
||||
|
||||
|
||||
## Struttura directory
|
||||
## Directory structure
|
||||
|
||||
```
|
||||
connectors/
|
||||
├── index.json ← lista ordinata degli id dei connector (input per compile.py)
|
||||
├── connectors.json ← INDICE COMPILATO (generato da compile.py, non editare)
|
||||
├── compile.py ← genera connectors.json
|
||||
├── index.html ← Catalogo UI (legge connectors.json via fetch)
|
||||
├── 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 (client-side, no backend)
|
||||
├── gmail/ ← Un connector per cartella
|
||||
│ ├── fragment.json ← Frammento dell'indice (id, name, type, ..., SENZA files[])
|
||||
│ ├── connector.json ← Configurazione tecnica
|
||||
│ ├── gmail_mcp_server.py ← Script MCP
|
||||
│ ├── gmail_oauth_setup.py ← Script setup OAuth
|
||||
│ ├── requirements.txt ← Dipendenze Python
|
||||
│ ├── icon_sm.svg ← Icona piccola (48×48)
|
||||
│ └── icon_lg.svg ← Icona grande (es. 96×96)
|
||||
│ └── 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
|
||||
@@ -143,27 +86,13 @@ connectors/
|
||||
│ ├── requirements.txt
|
||||
│ ├── icon_sm.svg
|
||||
│ └── icon_lg.svg
|
||||
├── ssh/
|
||||
│ ├── fragment.json
|
||||
│ ├── connector.json
|
||||
│ ├── ssh_mcp_server.py
|
||||
│ ├── requirements.txt
|
||||
│ ├── icon_sm.svg
|
||||
│ └── icon_lg.svg
|
||||
└── tavily/
|
||||
├── fragment.json
|
||||
├── connector.json
|
||||
├── verify.py
|
||||
├── icon_sm.png
|
||||
└── icon_lg.png
|
||||
```
|
||||
└── icon_lg.png
|
||||
└── ...
|
||||
```
|
||||
|
||||
## Schema — connectors.json (root)
|
||||
|
||||
Questo è l'**unico punto di fiducia**. Contiene `type`, `scope` e gli sha256 dei file di ogni connector.
|
||||
Non ha hash di sé stesso — in futuro potrà essere firmato digitalmente.
|
||||
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.
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -192,8 +121,6 @@ Non ha hash di sé stesso — in futuro potrà essere firmato digitalmente.
|
||||
"version_string": "1.0.0",
|
||||
"version_release_date": "2026-07-19",
|
||||
"files": [
|
||||
| `auth` | per OAuth | Oggetto con `type`, `provider`, `scopes` per badge UI (NO `deliver` qui, è nel manifest) |
|
||||
|
||||
{"path": "gmail_mcp_server.py", "sha256": "a50d4da9621f7a4b092f...", "size": 46772},
|
||||
{"path": "gmail_oauth_setup.py", "sha256": "e488acb289c43a3e6d54...", "size": 3627},
|
||||
{"path": "icon_lg.svg", "sha256": "93c8d9c8dae96f0206e5...", "size": 254},
|
||||
@@ -205,28 +132,28 @@ Non ha hash di sé stesso — in futuro potrà essere firmato digitalmente.
|
||||
}
|
||||
```
|
||||
|
||||
### Campi dell'indice
|
||||
### Index fields
|
||||
|
||||
| Campo | Obbligatorio | Descrizione |
|
||||
|-------|-------------|-------------|
|
||||
| `id` | ✅ | Identificatore unico (kebab-case) |
|
||||
| `name` | ✅ | Nome visualizzato |
|
||||
| `type` | ✅ | `mcp_remote` o `mcp_local` |
|
||||
| `scope` | ✅ | `global` o `user` |
|
||||
| `icon_small` | ✅ | Path relativo dalla root del marketplace |
|
||||
| `icon_large` | ✅ | Path relativo dalla root del marketplace |
|
||||
| `user_description` | ✅ | Descrizione breve per la UI |
|
||||
| `requires` | ✅ | Array di enum requisiti |
|
||||
| `tags` | ✅ | Array di tag per filtraggio |
|
||||
| `folder` | ✅ | Nome della cartella del connector |
|
||||
| `version` | ✅ | Intero per-connector, +1 a ogni modifica dei file |
|
||||
| `version_string` | ✅ | Semver (solo display) |
|
||||
| `version_release_date` | ✅ | Data ISO 8601 YYYY-MM-DD (solo display) |
|
||||
| `files` | ✅ | Array di file con sha256 (NO self-hash) |
|
||||
| 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 cartella)
|
||||
## Schema — connector.json (per folder)
|
||||
|
||||
Configurazione tecnica per l'attivazione del connector.
|
||||
Technical configuration for connector activation.
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -280,63 +207,63 @@ Configurazione tecnica per l'attivazione del connector.
|
||||
}
|
||||
```
|
||||
|
||||
### Campi del connector.json
|
||||
### connector.json fields
|
||||
|
||||
| Campo | Obbligatorio | Descrizione |
|
||||
|-------|-------------|-------------|
|
||||
| `id` | ✅ | Identificatore unico (match con folder name) |
|
||||
| `name` | ✅ | Nome visualizzato |
|
||||
| `version` | ✅ | Intero per-connector, +1 a ogni modifica dei file |
|
||||
| `version_string` | ✅ | Semver (solo display) |
|
||||
| `version_release_date` | ✅ | Data ISO 8601 YYYY-MM-DD (solo display) |
|
||||
| `type` | ✅ | `mcp_remote` o `mcp_local` |
|
||||
| `scope` | ✅ | `global` o `user` |
|
||||
| `requires` | ✅ | Array di enum requisiti |
|
||||
| `tags` | ✅ | Array di tag |
|
||||
| `auth` | ✅ | Oggetto configurazione autenticazione |
|
||||
| `docs` | ✅ | Array di documentazione multilingua. **`llm_short_description`** è il campo che finisce nel system prompt dell'LLM — deve descrivere COSA FA il connector, non elencare i tool (l'LLM li vede dopo `activate_tools`). Esempio: *"Weather — current conditions, 16-day forecast, and AQI data for any location."*
|
||||
| `icon_small` | ✅ | Nome file icona nella cartella locale |
|
||||
| `icon_large` | ✅ | Nome file icona nella cartella locale |
|
||||
| `launch_command` | solo `mcp_local` | Comando per avviare il server MCP |
|
||||
| `transport` | solo `mcp_local` | `stdio` (default) |
|
||||
| `dependencies` | consigliato | Dipendenze Python/Node (array vuoto se solo stdlib) |
|
||||
| `env` | se `requires` include `ENV` | Variabili d'ambiente che l'utente deve fornire (schema per la UI) — vedi § Campo env |
|
||||
| `setup_instructions` | consigliato | Passi per configurare il connector |
|
||||
| `mcp_config` | solo `mcp_local` | Configurazione per l'MCP client |
|
||||
| `homepage` | opzionale | URL del servizio |
|
||||
| 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 |
|
||||
|
||||
## Enum riservati
|
||||
## Reserved enums
|
||||
|
||||
### type (tipo di connector)
|
||||
### type (connector type)
|
||||
|
||||
| Valore | Descrizione | Esempi |
|
||||
|--------|-------------|--------|
|
||||
| `mcp_remote` | Server MCP hosted, accessibile via URL | Tavily, Weather |
|
||||
| `mcp_local` | Script da eseguire localmente | Gmail, Google Calendar, WhatsApp |
|
||||
| `script` | Script standalone (non MCP) | *(futuro)* |
|
||||
| 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 (ambito di configurazione)
|
||||
### scope (configuration scope)
|
||||
|
||||
| Valore | Descrizione | Esempi |
|
||||
|--------|-------------|--------|
|
||||
| `global` | Una singola istanza/config per tutto il sistema | Tavily, Weather, Google Trends |
|
||||
| `user` | Ogni utente ha la propria istanza/autenticazione | Gmail, WhatsApp, Google Calendar |
|
||||
| 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 (prerequisiti)
|
||||
### requires (prerequisites)
|
||||
|
||||
| Valore | Descrizione |
|
||||
|--------|-------------|
|
||||
| `API_KEY` | Richiede una chiave API da configurare |
|
||||
| `OAUTH` | Richiede autenticazione OAuth (Google, ecc.) |
|
||||
| `DOCKER` | Richiede Docker Engine |
|
||||
| `NODE` | Richiede Node.js runtime |
|
||||
| `PYTHON` | Richiede Python 3 |
|
||||
| `SECRETS_DIR` | ❌ **Deprecato** — la cartella `secrets/` è rimossa dal modello; i connector devono usare `ENV`/`SECRET` (vedi § Sintassi placeholder) |
|
||||
| `ENV` | Richiede variabili d'ambiente (dichiarate nel campo `env` del manifest) |
|
||||
| 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) |
|
||||
|
||||
## Campo auth
|
||||
## The auth field
|
||||
|
||||
Struttura che descrive come il connector gestisce l'autenticazione:
|
||||
Structure describing how the connector handles authentication:
|
||||
|
||||
```json
|
||||
// API key in query string
|
||||
@@ -345,112 +272,113 @@ Struttura che descrive come il connector gestisce l'autenticazione:
|
||||
// API key in header
|
||||
{"type": "api_key", "delivery": "header", "param": "X-API-Key"}
|
||||
|
||||
// OAuth2 — provider SOLO slug (Skald risolve endpoint + client secrets)
|
||||
// OAuth2 — provider is ONLY a slug (Skald resolves endpoints + client secrets)
|
||||
{"type": "oauth2", "provider": "google", "scopes": ["...", "..."]}
|
||||
|
||||
// OAuth2 con deliver (Skald inietta il JSON authorized_user via env var)
|
||||
// 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 con deliver su file (legacy)
|
||||
// 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 fornita via variabili d'ambiente
|
||||
// Password / app-password provided via environment variables
|
||||
{"type": "password", "delivery": "env"}
|
||||
|
||||
// Nessuna autenticazione
|
||||
// No authentication
|
||||
{"type": "none"}
|
||||
```
|
||||
|
||||
### Campo deliver (solo OAuth2)
|
||||
### The deliver field (OAuth2 only)
|
||||
|
||||
Dichiara **come** Skald consegna la credenziale OAuth ottenuta al processo del server MCP.
|
||||
Declares **how** Skald delivers the obtained OAuth credential to the MCP server process.
|
||||
|
||||
| Campo | Obbligatorio | Descrizione |
|
||||
|-------|-------------|-------------|
|
||||
| `as` | ✅ | `"file"` (su disco) o `"env"` (variabile d'ambiente) |
|
||||
| `format` | ✅ | Nome della serializzazione — es. `"google_authorized_user"` (JSON Google che `from_authorized_user_file` legge), `"refresh_token"`, `"access_token"` |
|
||||
| `path` | solo `as=file` | Path con placeholder `{secrets}` (Skald lo espande a dir per-utente a runtime). DEVE matchare il path in `mcp_config.env`. |
|
||||
| `env` | solo `as=env` | Nome della variabile d'ambiente in cui Skald inietta l'intero JSON authorized_user. **Non va dichiarata in `mcp_config.env`** — Skald la inietta a runtime. |
|
||||
| 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. |
|
||||
|
||||
Il feed NON contiene MAI: `client_id`, `client_secret`, `endpoint` URL, `redirect_uri`. Questi sono risolti lato Skald a partire dal nome del `provider`.
|
||||
The feed NEVER contains: `client_id`, `client_secret`, `endpoint` URL, `redirect_uri`. These are resolved on the Skald side from the `provider` name.
|
||||
|
||||
## Campo env (variabili d'ambiente)
|
||||
## The env field (environment variables)
|
||||
|
||||
Usato quando `requires` include `ENV`. È un **array** che dichiara le variabili
|
||||
d'ambiente che l'utente deve fornire per far funzionare il connector; **nessuna
|
||||
credenziale finisce su disco né in `secrets/`** — l'host raccoglie i valori,
|
||||
obbliga la compilazione dei campi obbligatori, e li inietta come environment
|
||||
nel processo del server MCP al lancio. Il server le legge da `os.environ`.
|
||||
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`.
|
||||
|
||||
```json
|
||||
"env": [
|
||||
{
|
||||
"name": "EMAIL_IMAP_HOST", // nome della variabile d'ambiente
|
||||
"label": "IMAP host", // etichetta per la UI
|
||||
"description": "IMAP server hostname (es. imap.gmail.com)",
|
||||
"required": true, // se true, l'host deve obbligare la compilazione
|
||||
"secret": false, // se true, la UI la maschera e la tratta come segreto
|
||||
"example": "imap.gmail.com" // placeholder/esempio (opzionale)
|
||||
"name": "EMAIL_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 o app-password del provider",
|
||||
"description": "Password or app-password of the provider",
|
||||
"required": true,
|
||||
"secret": true,
|
||||
"default": "" // valore di default se non obbligatorio (opzionale)
|
||||
"default": "" // default value if not required (optional)
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
| Campo | Obbligatorio | Descrizione |
|
||||
|-------|-------------|-------------|
|
||||
| `name` | ✅ | Nome della variabile d'ambiente (UPPER_SNAKE_CASE) |
|
||||
| `label` | ✅ | Etichetta breve per la UI |
|
||||
| `description` | ✅ | Testo di aiuto |
|
||||
| `required` | ✅ | Se `true`, l'host obbliga l'utente a fornire un valore |
|
||||
| `secret` | consigliato | Se `true`, valore sensibile (mascherato, non loggato) |
|
||||
| `default` | opzionale | Valore usato se non fornito (solo per non obbligatorie) |
|
||||
| `example` | opzionale | Placeholder di esempio per la UI |
|
||||
| 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 |
|
||||
|
||||
## Sintassi placeholder (unificata)
|
||||
## Placeholder syntax (unified)
|
||||
|
||||
Ogni valore che skald deve riempire a runtime con un dato fornito dall'utente
|
||||
usa **uno di due token**, ovunque compaia (URL, `mcp_config.env`, `verify.command`):
|
||||
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 | Significato | Esempio |
|
||||
|-------|-------------|---------|
|
||||
| `{ENV:NAME}` | Variabile non sensibile (hostname, porta, username…) | `{ENV:EMAIL_IMAP_HOST}` |
|
||||
| `{SECRET:NAME}` | Variabile sensibile (password, API key, token) | `{SECRET:EMAIL_PASSWORD}` |
|
||||
| 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` è il nome dichiarato nell'array `env[]` (campo `name`). skald raccoglie
|
||||
i valori tramite un form (maschera i `{SECRET:}`), li inietta come environment
|
||||
nel processo del server MCP / verify, e sostituisce i token nel manifest.
|
||||
`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.
|
||||
|
||||
Regole:
|
||||
- I token non riconosciuti (es. `{secrets}/…`, legacy `{key}`, `{env:NAME}`)
|
||||
sono **deprecati**: skald non li sostituisce e il manifest va aggiornato.
|
||||
- `{SECRET:<auth.param>}` è riservato alla chiave primaria quando
|
||||
`auth.type = "api_key"` (es. Tavily: `{SECRET:tavilyApiKey}`). skald tratta
|
||||
quel valore anche come API key per il routing bearer/header.
|
||||
- Un token `{ENV:X}` o `{SECRET:X}` la cui `X` non è nell'`env[]` del manifest
|
||||
viene sostituito con stringa vuota (l'host non può indovinarlo).
|
||||
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 when
|
||||
`auth.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 whose `X` is not in the manifest's `env[]`
|
||||
is substituted with an empty string (the host cannot guess it).
|
||||
|
||||
### Deprecations
|
||||
|
||||
| Token | Stato | Sostituzione |
|
||||
|-------|-------|--------------|
|
||||
| `{key}` | ❌ deprecato | `{SECRET:<auth.param>}` |
|
||||
| `{env:NAME}` | ❌ deprecato | `{ENV:NAME}` |
|
||||
| `{secrets}/…` | ❌ deprecato | Il connector deve dichiarare il path come `{ENV:…}` (la cartella `secrets/` è rimossa dal modello) |
|
||||
| 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) |
|
||||
|
||||
## Campo verify (test prima del salvataggio)
|
||||
## The verify field (test before save)
|
||||
|
||||
Dichiara un comando shell che skald esegue **dopo** che l'utente ha compilato
|
||||
il form e **prima** di persistere l'attivazione. Serve a verificare che le
|
||||
credenziali appena inserite funzionino davvero.
|
||||
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.
|
||||
|
||||
```json
|
||||
"verify": {
|
||||
@@ -459,174 +387,145 @@ credenziali appena inserite funzionino davvero.
|
||||
}
|
||||
```
|
||||
|
||||
| Campo | Obbligatorio | Descrizione |
|
||||
|-------|-------------|-------------|
|
||||
| `command` | ✅ | Comando shell. Gira nello stesso sandbox del server: container `skald-{userid}` per `mcp_local` user, host per `mcp_remote` global. Le env/secret dichiarati sono iniettate |
|
||||
| `timeout_secs` | opzionale | Default 15. skald killa il processo allo scadere |
|
||||
| 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 |
|
||||
|
||||
### Convenzione output
|
||||
### Output convention
|
||||
|
||||
Il comando deve stampare **un singolo oggetto JSON su stdout** e nient'altro:
|
||||
The command must print **a single JSON object on stdout** and nothing else:
|
||||
|
||||
```json
|
||||
{"ok": true, "message": "IMAP and SMTP authentication successful", "details": {"imap": "...", "smtp": "..."}}
|
||||
{"ok": false, "message": "IMAP login failed: INVALID_CREDENTIALS"}
|
||||
```
|
||||
|
||||
| Campo | Tipo | Descrizione |
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `ok` | bool | `true` = test passato |
|
||||
| `message` | string | Messaggio mostrato all'utente (mai loggare secret qui dentro) |
|
||||
| `details` | object | Opzionale, dettagli strutturati mostrati in `<pre>` |
|
||||
| `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 su successo, ≠ 0 su fallimento (skald usa l'exit code come
|
||||
fallback se il parse JSON fallisce). **Mai stampare credenziali** nel
|
||||
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`.
|
||||
|
||||
### Dove mettere lo script
|
||||
### Where to put the script
|
||||
|
||||
Se `command` referenzia un file (es. `verify.py`), il file va:
|
||||
1. Aggiunto all'array `files[]` in `connectors.json` (con `sha256` e `size`)
|
||||
2. Salvato nella cartella del connector (`<id>/verify.py`)
|
||||
If `command` references a file (e.g. `verify.py`), the file must:
|
||||
1. Be added to the `files[]` array in `connectors.json` (with `sha256` and `size`)
|
||||
2. Be saved in the connector folder (`<id>/verify.py`)
|
||||
|
||||
skald lo scarica, ne verifica lo SHA-256 contro l'indice, e lo rende disponibile
|
||||
nello stesso path del server principale (container per i `mcp_local`, dir
|
||||
`./scripts/<id>/` sull'host per i `mcp_remote`).
|
||||
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).
|
||||
|
||||
### Senza verify
|
||||
### Without verify
|
||||
|
||||
Se `verify` manca, skald **non esegue nessun test** — l'attivazione è diretta
|
||||
come oggi. Il connector va in `auth_state='ready'` senza verifica. Per i
|
||||
`mcp_remote` non c'è fallback handshake: l'autore del manifest decide se vuole
|
||||
il test scrivendo `verify`.
|
||||
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`.
|
||||
|
||||
## Convenzioni icone
|
||||
## Icon conventions
|
||||
|
||||
- **Formato**: SVG per icone vettoriali (meglio per retina/zoom), PNG per raster
|
||||
- **Nome**: `icon_sm.{svg|png}` (small, ~48×48px), `icon_lg.{svg|png}` (large, ~96×96px)
|
||||
- **Path**: relativo alla cartella del connector
|
||||
- **Nell'indice** la path è `{folder}/{filename}` (es. `gmail/icon_sm.svg`)
|
||||
- **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`)
|
||||
|
||||
## Integrità file (sha256)
|
||||
## File integrity (sha256)
|
||||
|
||||
- Gli SHA-256 sono generati **automaticamente** da `scripts/compile.py` a partire
|
||||
dai file fisici presenti in ogni cartella — mai manuali, mai stale.
|
||||
- L'unico file firmato (in futuro) sarà `connectors.json` (l'indice).
|
||||
- `fragment.json` e `index.json` non hanno hash — sono solo input di compilazione.
|
||||
- SHA-256 hashes are generated **automatically** by `scripts/compile.py` from
|
||||
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.json` and `index.json` have no hash — they are just compilation inputs.
|
||||
|
||||
## Struttura directory
|
||||
## Local workflow
|
||||
|
||||
```
|
||||
connectors/
|
||||
├── index.json ← lista ordinata degli id dei connector (input per compile.py)
|
||||
├── connectors.json ← INDICE COMPILATO (generato da compile.py, non editare)
|
||||
├── compile.py ← genera connectors.json (lanciato da scripts/compile.py)
|
||||
├── index.html ← Catalogo UI (legge connectors.json via fetch)
|
||||
├── oauth/
|
||||
│ └── show.html ← OAuth callback receiver
|
||||
├── gmail/ ← Un connector per cartella
|
||||
│ ├── fragment.json ← Frammento dell'indice (id, name, type, ..., SENZA files[])
|
||||
│ ├── connector.json ← Configurazione tecnica (mcp_config, auth.deliver, ...)
|
||||
│ ├── gmail_mcp_server.py ← Script MCP
|
||||
│ ├── gmail_oauth_setup.py ← Script setup OAuth
|
||||
│ ├── requirements.txt ← Dipendenze Python
|
||||
│ ├── icon_sm.svg ← Icona piccola (48×48)
|
||||
│ └── icon_lg.svg ← Icona grande (es. 96×96)
|
||||
├── email/
|
||||
│ ├── fragment.json
|
||||
│ ├── connector.json
|
||||
│ ├── email_mcp_server.py
|
||||
│ ├── verify.py
|
||||
│ ├── requirements.txt
|
||||
│ ├── icon_sm.svg
|
||||
│ └── icon_lg.svg
|
||||
└── ...
|
||||
```
|
||||
1. **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`
|
||||
|
||||
## Workflow locale
|
||||
2. **Modifying an existing connector**:
|
||||
- Edit the files in the connector folder
|
||||
- **Do not touch** `connectors.json` — it gets regenerated
|
||||
- Run `python3 scripts/compile.py`
|
||||
|
||||
1. **Aggiungere un nuovo connector**:
|
||||
- Crea cartella `connectors/<id>/`
|
||||
- Crea `fragment.json` (id, name, type, scope, icone, auth, tools, ...)
|
||||
- Crea `connector.json` (config tecnica: mcp_config, launch_command, ...)
|
||||
- Aggiungi script MCP, icone, verify.py, requirements.txt
|
||||
- Aggiungi l'id a `connectors/index.json`
|
||||
- Lancia `python3 scripts/compile.py`
|
||||
|
||||
2. **Modificare un connector esistente**:
|
||||
- Modifica i file nella cartella del connector
|
||||
- **Non toccare** `connectors.json` — viene rigenerato
|
||||
- Lancia `python3 scripts/compile.py`
|
||||
|
||||
3. **Prima del deploy**:
|
||||
3. **Before deploying**:
|
||||
```bash
|
||||
python3 scripts/compile.py # rigenera connectors.json con SHA-256 fresh
|
||||
python3 scripts/compile.py --verify # (opzionale) verifica che sia aggiornato
|
||||
python3 scripts/compile.py # regenerates connectors.json with fresh SHA-256s
|
||||
python3 scripts/compile.py --verify # (optional) verifies that it is up to date
|
||||
```
|
||||
|
||||
4. **Deploy sul server**:
|
||||
4. **Deploy to the server**:
|
||||
```bash
|
||||
ssh dguiducci@skald-home-server /home/dguiducci/marketplace_deploy.sh
|
||||
```
|
||||
oppure con MCP SSH:
|
||||
or with MCP SSH:
|
||||
```bash
|
||||
# via mcp__ssh__exec alias "skald-home-server"
|
||||
/home/dguiducci/marketplace_deploy.sh
|
||||
```
|
||||
(Il deploy script fa `git pull` + `cp -r connectors/* /var/www/connectors.skaldagent.net/`)
|
||||
(The deploy script does `git pull` + `cp -r connectors/* /var/www/connectors.skaldagent.net/`)
|
||||
|
||||
5. Verifica su `https://connectors.skaldagent.net/`
|
||||
5. Verify on `https://connectors.skaldagent.net/`
|
||||
|
||||
## Fragment.json
|
||||
|
||||
Vedi [docs/connector.manifest_guide.md](docs/connector.manifest_guide.md) per la guida completa
|
||||
alla creazione.
|
||||
See [docs/connector.manifest_guide.md](docs/connector.manifest_guide.md) for the complete
|
||||
creation guide.
|
||||
|
||||
`fragment.json` contiene tutti i campi dell'entry di `connectors.json` **tranne** `files[]`.
|
||||
Questi sono i campi obbligatori:
|
||||
`fragment.json` contains all the fields of a `connectors.json` entry **except** `files[]`.
|
||||
These are the required fields:
|
||||
|
||||
| Campo | Obbligatorio | Descrizione |
|
||||
|-------|-------------|-------------|
|
||||
| `id` | ✅ | Identificatore unico (match con folder name) |
|
||||
| `name` | ✅ | Nome visualizzato |
|
||||
| `type` | ✅ | `mcp_remote` o `mcp_local` |
|
||||
| `scope` | ✅ | `global` o `user` |
|
||||
| `icon_small` | ✅ | Path relativo dalla root del marketplace |
|
||||
| `icon_large` | ✅ | Path relativo dalla root del marketplace |
|
||||
| `user_description` | ✅ | Descrizione breve per la UI |
|
||||
| `requires` | ✅ | Array di enum requisiti |
|
||||
| `tags` | ✅ | Array di tag per filtraggio |
|
||||
| `folder` | ✅ | Nome della cartella del connector (match con `id`) |
|
||||
| `version` | ✅ | Intero per-connector, +1 a ogni modifica dei file |
|
||||
| `version_string` | ✅ | Semver (solo display) |
|
||||
| `version_release_date` | ✅ | Data ISO 8601 (solo display) |
|
||||
| `tools` | ✅ | Array di `{name, display_name}` per friendly names UI |
|
||||
| `auth` | opzionale | Configurazione autenticazione (se diversa da `"none"`) |
|
||||
| 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"`) |
|
||||
|
||||
Nota: l'array `files[]` (con SHA-256 e size) viene aggiunto **automaticamente**
|
||||
da `compile.py` scansionando i file presenti nella cartella — non va mai scritto
|
||||
a mano.
|
||||
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 su server remoto
|
||||
## Deploy to remote server
|
||||
|
||||
Il server remoto è:
|
||||
The remote server is:
|
||||
|
||||
- **Host**: skald-home-server (192.168.1.100 / 145.40.169.107)
|
||||
- **User**: dguiducci
|
||||
- **Path**: `/var/www/connectors.skaldagent.net/`
|
||||
- **Proprietario**: `caddy:caddy`
|
||||
- **Sudo**: richiesto per scrivere in `/var/www/`
|
||||
- **Owner**: `caddy:caddy`
|
||||
- **Sudo**: required to write in `/var/www/`
|
||||
|
||||
## Connector attuali
|
||||
## Current connectors
|
||||
|
||||
| ID | Nome | Tipo | Scope | Auth | Verify |
|
||||
| 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`) | ⏳ Fase 2 — OAuth via loopback listener |
|
||||
| `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) |
|
||||
@@ -637,6 +536,8 @@ Il server remoto è:
|
||||
| `google-trends` | Google Trends | `mcp_local` | `global` | none | `verify.py` (trendspyg import probe) |
|
||||
| `wikipedia` | Wikipedia | `mcp_local` | `global` | none | — |
|
||||
| `whatsapp` | WhatsApp | `mcp_local` | `user` | qr | — |
|
||||
**Stato del verify-before-save in skald**: `exa`, `drive`, `email`, `tavily`, e `gcal` hanno `verify` completo
|
||||
(script + JSON output); `gmail` aspetta la Fase 2 (OAuth via loopback listener).
|
||||
Un connector senza `verify` viene attivato senza test — vedi § Senza verify.
|
||||
| `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.
|
||||
|
||||
@@ -1245,6 +1245,164 @@
|
||||
"size": 5021
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "playwright",
|
||||
"name": "Playwright",
|
||||
"type": "mcp_local",
|
||||
"scope": "global",
|
||||
"icon_small": "playwright/icon_sm.svg",
|
||||
"icon_large": "playwright/icon_lg.svg",
|
||||
"user_description": "Full browser automation \u2014 navigate, click, type, and read any web page with a real headless Chromium.",
|
||||
"requires": [
|
||||
"NODE"
|
||||
],
|
||||
"tags": [
|
||||
"browser",
|
||||
"automation",
|
||||
"mcp",
|
||||
"local",
|
||||
"playwright",
|
||||
"web",
|
||||
"scraping"
|
||||
],
|
||||
"auth": {
|
||||
"type": "none"
|
||||
},
|
||||
"folder": "playwright",
|
||||
"version": 1,
|
||||
"version_string": "1.0.0",
|
||||
"version_release_date": "2026-08-19",
|
||||
"tools": [
|
||||
{
|
||||
"name": "browser_close",
|
||||
"display_name": "Close Browser"
|
||||
},
|
||||
{
|
||||
"name": "browser_resize",
|
||||
"display_name": "Resize Browser Window"
|
||||
},
|
||||
{
|
||||
"name": "browser_console_messages",
|
||||
"display_name": "Get Console Messages"
|
||||
},
|
||||
{
|
||||
"name": "browser_handle_dialog",
|
||||
"display_name": "Handle a Dialog"
|
||||
},
|
||||
{
|
||||
"name": "browser_evaluate",
|
||||
"display_name": "Evaluate JavaScript"
|
||||
},
|
||||
{
|
||||
"name": "browser_file_upload",
|
||||
"display_name": "Upload Files"
|
||||
},
|
||||
{
|
||||
"name": "browser_drop",
|
||||
"display_name": "Drop onto Element"
|
||||
},
|
||||
{
|
||||
"name": "browser_find",
|
||||
"display_name": "Find in Page Snapshot"
|
||||
},
|
||||
{
|
||||
"name": "browser_fill_form",
|
||||
"display_name": "Fill Form"
|
||||
},
|
||||
{
|
||||
"name": "browser_press_key",
|
||||
"display_name": "Press a Key"
|
||||
},
|
||||
{
|
||||
"name": "browser_type",
|
||||
"display_name": "Type Text"
|
||||
},
|
||||
{
|
||||
"name": "browser_navigate",
|
||||
"display_name": "Navigate to URL"
|
||||
},
|
||||
{
|
||||
"name": "browser_navigate_back",
|
||||
"display_name": "Go Back"
|
||||
},
|
||||
{
|
||||
"name": "browser_network_requests",
|
||||
"display_name": "List Network Requests"
|
||||
},
|
||||
{
|
||||
"name": "browser_network_request",
|
||||
"display_name": "Show Network Request"
|
||||
},
|
||||
{
|
||||
"name": "browser_run_code_unsafe",
|
||||
"display_name": "Run Playwright Code (Unsafe)"
|
||||
},
|
||||
{
|
||||
"name": "browser_take_screenshot",
|
||||
"display_name": "Take a Screenshot"
|
||||
},
|
||||
{
|
||||
"name": "browser_snapshot",
|
||||
"display_name": "Page Snapshot"
|
||||
},
|
||||
{
|
||||
"name": "browser_click",
|
||||
"display_name": "Click"
|
||||
},
|
||||
{
|
||||
"name": "browser_drag",
|
||||
"display_name": "Drag Mouse"
|
||||
},
|
||||
{
|
||||
"name": "browser_hover",
|
||||
"display_name": "Hover Mouse"
|
||||
},
|
||||
{
|
||||
"name": "browser_select_option",
|
||||
"display_name": "Select Option"
|
||||
},
|
||||
{
|
||||
"name": "browser_tabs",
|
||||
"display_name": "Manage Tabs"
|
||||
},
|
||||
{
|
||||
"name": "browser_wait_for",
|
||||
"display_name": "Wait For"
|
||||
}
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "connector.json",
|
||||
"sha256": "aa9e6ac53b7c7abaa595ce99fcf903e0fffffb28f385c4d9d5107d16be4c8978",
|
||||
"size": 4031
|
||||
},
|
||||
{
|
||||
"path": "icon_lg.svg",
|
||||
"sha256": "90422e9cbda3f8ef0ec7c00fbe7d6769ba468aff81dbb5e6e48124ef245fd6a4",
|
||||
"size": 5031
|
||||
},
|
||||
{
|
||||
"path": "icon_sm.svg",
|
||||
"sha256": "aa13f9f5ac5c734b869bd6b005347fd05dd70aa8979d197cb5c79ec8eabfb89d",
|
||||
"size": 5031
|
||||
},
|
||||
{
|
||||
"path": "index.js",
|
||||
"sha256": "7bbdee60a27013fa3b4b139d90b83f3c72c25520693e76c6e8860d7085446dc8",
|
||||
"size": 1537
|
||||
},
|
||||
{
|
||||
"path": "package.json",
|
||||
"sha256": "59ebf53dfda2126ab6ea352c6f91a8226b7d30dbd22d4056814450fb67b96ec7",
|
||||
"size": 390
|
||||
},
|
||||
{
|
||||
"path": "verify.js",
|
||||
"sha256": "2588ce8dd872ba83ff4266353f95315288377c0d30651ac3bdaa561f6dfa3b23",
|
||||
"size": 1987
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
["gmail", "gcal", "drive", "email", "exa", "firecrawl", "http-fetch", "serpapi-flights", "ssh", "tavily", "weather", "whatsapp", "wikipedia", "context7", "gmaps", "google-trends", "linkedin"]
|
||||
["gmail", "gcal", "drive", "email", "exa", "firecrawl", "http-fetch", "serpapi-flights", "ssh", "tavily", "weather", "whatsapp", "wikipedia", "context7", "gmaps", "google-trends", "linkedin", "playwright"]
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
{
|
||||
"id": "playwright",
|
||||
"name": "Playwright",
|
||||
"type": "mcp_local",
|
||||
"requires": [
|
||||
"NODE"
|
||||
],
|
||||
"setup_instructions": [
|
||||
"Requires Node.js 18+ on the host that runs the connector.",
|
||||
"On install, the package postinstall downloads Chromium (~350 MB) into the Playwright browsers cache.",
|
||||
"No configuration needed — the browser runs headless with an isolated in-memory profile: no cookies or login state are kept between sessions.",
|
||||
"Security note: the default tool set includes browser_run_code_unsafe and browser_evaluate, which execute arbitrary JavaScript (RCE-equivalent). Enable only for trusted agents."
|
||||
],
|
||||
"docs": [
|
||||
{
|
||||
"lang": "en",
|
||||
"description": "Full browser automation with a real headless Chromium: navigate any web page, click, type, fill forms, handle dialogs, manage tabs, take accessibility snapshots and screenshots, and inspect network requests. Runs headless with an isolated in-memory profile (no state is kept between sessions). Includes browser_run_code_unsafe / browser_evaluate, which execute arbitrary JavaScript (RCE-equivalent) — enable only for trusted agents.",
|
||||
"llm_short_description": "Browser automation — navigate web pages, click, type, fill forms, take accessibility snapshots and screenshots, manage tabs, and inspect network requests with a real headless Chromium."
|
||||
}
|
||||
],
|
||||
"auth": {
|
||||
"type": "none"
|
||||
},
|
||||
"mcp_config": {
|
||||
"command": "node",
|
||||
"args": [
|
||||
"index.js"
|
||||
],
|
||||
"transport": "stdio"
|
||||
},
|
||||
"verify": {
|
||||
"command": "node verify.js",
|
||||
"timeout_secs": 30
|
||||
},
|
||||
"homepage": "https://playwright.dev",
|
||||
"icon_small": "icon_sm.svg",
|
||||
"icon_large": "icon_lg.svg",
|
||||
"scope": "global",
|
||||
"tags": [
|
||||
"browser",
|
||||
"automation",
|
||||
"mcp",
|
||||
"local",
|
||||
"playwright",
|
||||
"web",
|
||||
"scraping"
|
||||
],
|
||||
"version": 1,
|
||||
"version_string": "1.0.0",
|
||||
"version_release_date": "2026-08-19",
|
||||
"tools": [
|
||||
{
|
||||
"name": "browser_close",
|
||||
"display_name": "Close Browser"
|
||||
},
|
||||
{
|
||||
"name": "browser_resize",
|
||||
"display_name": "Resize Browser Window"
|
||||
},
|
||||
{
|
||||
"name": "browser_console_messages",
|
||||
"display_name": "Get Console Messages"
|
||||
},
|
||||
{
|
||||
"name": "browser_handle_dialog",
|
||||
"display_name": "Handle a Dialog"
|
||||
},
|
||||
{
|
||||
"name": "browser_evaluate",
|
||||
"display_name": "Evaluate JavaScript"
|
||||
},
|
||||
{
|
||||
"name": "browser_file_upload",
|
||||
"display_name": "Upload Files"
|
||||
},
|
||||
{
|
||||
"name": "browser_drop",
|
||||
"display_name": "Drop onto Element"
|
||||
},
|
||||
{
|
||||
"name": "browser_find",
|
||||
"display_name": "Find in Page Snapshot"
|
||||
},
|
||||
{
|
||||
"name": "browser_fill_form",
|
||||
"display_name": "Fill Form"
|
||||
},
|
||||
{
|
||||
"name": "browser_press_key",
|
||||
"display_name": "Press a Key"
|
||||
},
|
||||
{
|
||||
"name": "browser_type",
|
||||
"display_name": "Type Text"
|
||||
},
|
||||
{
|
||||
"name": "browser_navigate",
|
||||
"display_name": "Navigate to URL"
|
||||
},
|
||||
{
|
||||
"name": "browser_navigate_back",
|
||||
"display_name": "Go Back"
|
||||
},
|
||||
{
|
||||
"name": "browser_network_requests",
|
||||
"display_name": "List Network Requests"
|
||||
},
|
||||
{
|
||||
"name": "browser_network_request",
|
||||
"display_name": "Show Network Request"
|
||||
},
|
||||
{
|
||||
"name": "browser_run_code_unsafe",
|
||||
"display_name": "Run Playwright Code (Unsafe)"
|
||||
},
|
||||
{
|
||||
"name": "browser_take_screenshot",
|
||||
"display_name": "Take a Screenshot"
|
||||
},
|
||||
{
|
||||
"name": "browser_snapshot",
|
||||
"display_name": "Page Snapshot"
|
||||
},
|
||||
{
|
||||
"name": "browser_click",
|
||||
"display_name": "Click"
|
||||
},
|
||||
{
|
||||
"name": "browser_drag",
|
||||
"display_name": "Drag Mouse"
|
||||
},
|
||||
{
|
||||
"name": "browser_hover",
|
||||
"display_name": "Hover Mouse"
|
||||
},
|
||||
{
|
||||
"name": "browser_select_option",
|
||||
"display_name": "Select Option"
|
||||
},
|
||||
{
|
||||
"name": "browser_tabs",
|
||||
"display_name": "Manage Tabs"
|
||||
},
|
||||
{
|
||||
"name": "browser_wait_for",
|
||||
"display_name": "Wait For"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
{
|
||||
"id": "playwright",
|
||||
"name": "Playwright",
|
||||
"type": "mcp_local",
|
||||
"scope": "global",
|
||||
"icon_small": "playwright/icon_sm.svg",
|
||||
"icon_large": "playwright/icon_lg.svg",
|
||||
"user_description": "Full browser automation — navigate, click, type, and read any web page with a real headless Chromium.",
|
||||
"requires": [
|
||||
"NODE"
|
||||
],
|
||||
"tags": [
|
||||
"browser",
|
||||
"automation",
|
||||
"mcp",
|
||||
"local",
|
||||
"playwright",
|
||||
"web",
|
||||
"scraping"
|
||||
],
|
||||
"auth": {
|
||||
"type": "none"
|
||||
},
|
||||
"folder": "playwright",
|
||||
"version": 1,
|
||||
"version_string": "1.0.0",
|
||||
"version_release_date": "2026-08-19",
|
||||
"tools": [
|
||||
{
|
||||
"name": "browser_close",
|
||||
"display_name": "Close Browser"
|
||||
},
|
||||
{
|
||||
"name": "browser_resize",
|
||||
"display_name": "Resize Browser Window"
|
||||
},
|
||||
{
|
||||
"name": "browser_console_messages",
|
||||
"display_name": "Get Console Messages"
|
||||
},
|
||||
{
|
||||
"name": "browser_handle_dialog",
|
||||
"display_name": "Handle a Dialog"
|
||||
},
|
||||
{
|
||||
"name": "browser_evaluate",
|
||||
"display_name": "Evaluate JavaScript"
|
||||
},
|
||||
{
|
||||
"name": "browser_file_upload",
|
||||
"display_name": "Upload Files"
|
||||
},
|
||||
{
|
||||
"name": "browser_drop",
|
||||
"display_name": "Drop onto Element"
|
||||
},
|
||||
{
|
||||
"name": "browser_find",
|
||||
"display_name": "Find in Page Snapshot"
|
||||
},
|
||||
{
|
||||
"name": "browser_fill_form",
|
||||
"display_name": "Fill Form"
|
||||
},
|
||||
{
|
||||
"name": "browser_press_key",
|
||||
"display_name": "Press a Key"
|
||||
},
|
||||
{
|
||||
"name": "browser_type",
|
||||
"display_name": "Type Text"
|
||||
},
|
||||
{
|
||||
"name": "browser_navigate",
|
||||
"display_name": "Navigate to URL"
|
||||
},
|
||||
{
|
||||
"name": "browser_navigate_back",
|
||||
"display_name": "Go Back"
|
||||
},
|
||||
{
|
||||
"name": "browser_network_requests",
|
||||
"display_name": "List Network Requests"
|
||||
},
|
||||
{
|
||||
"name": "browser_network_request",
|
||||
"display_name": "Show Network Request"
|
||||
},
|
||||
{
|
||||
"name": "browser_run_code_unsafe",
|
||||
"display_name": "Run Playwright Code (Unsafe)"
|
||||
},
|
||||
{
|
||||
"name": "browser_take_screenshot",
|
||||
"display_name": "Take a Screenshot"
|
||||
},
|
||||
{
|
||||
"name": "browser_snapshot",
|
||||
"display_name": "Page Snapshot"
|
||||
},
|
||||
{
|
||||
"name": "browser_click",
|
||||
"display_name": "Click"
|
||||
},
|
||||
{
|
||||
"name": "browser_drag",
|
||||
"display_name": "Drag Mouse"
|
||||
},
|
||||
{
|
||||
"name": "browser_hover",
|
||||
"display_name": "Hover Mouse"
|
||||
},
|
||||
{
|
||||
"name": "browser_select_option",
|
||||
"display_name": "Select Option"
|
||||
},
|
||||
{
|
||||
"name": "browser_tabs",
|
||||
"display_name": "Manage Tabs"
|
||||
},
|
||||
{
|
||||
"name": "browser_wait_for",
|
||||
"display_name": "Wait For"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<svg width="96" height="96" viewBox="0 0 400 400" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M136.444 221.556C123.558 225.213 115.104 231.625 109.535 238.032C114.869 233.364 122.014 229.08 131.652 226.348C141.51 223.554 149.92 223.574 156.869 224.915V219.481C150.941 218.939 144.145 219.371 136.444 221.556ZM108.946 175.876L61.0895 188.484C61.0895 188.484 61.9617 189.716 63.5767 191.36L104.153 180.668C104.153 180.668 103.578 188.077 98.5847 194.705C108.03 187.559 108.946 175.876 108.946 175.876ZM149.005 288.347C81.6582 306.486 46.0272 228.438 35.2396 187.928C30.2556 169.229 28.0799 155.067 27.5 145.928C27.4377 144.979 27.4665 144.179 27.5336 143.446C24.04 143.657 22.3674 145.473 22.7077 150.721C23.2876 159.855 25.4633 174.016 30.4473 192.721C41.2301 233.225 76.8659 311.273 144.213 293.134C158.872 289.185 169.885 281.992 178.152 272.81C170.532 279.692 160.995 285.112 149.005 288.347ZM161.661 128.11V132.903H188.077C187.535 131.206 186.989 129.677 186.447 128.11H161.661Z" fill="#2D4552"/>
|
||||
<path d="M193.981 167.584C205.861 170.958 212.144 179.287 215.465 186.658L228.711 190.42C228.711 190.42 226.904 164.623 203.57 157.995C181.741 151.793 168.308 170.124 166.674 172.496C173.024 167.972 182.297 164.268 193.981 167.584ZM299.422 186.777C277.573 180.547 264.145 198.916 262.535 201.255C268.89 196.736 278.158 193.031 289.837 196.362C301.698 199.741 307.976 208.06 311.307 215.436L324.572 219.212C324.572 219.212 322.736 193.41 299.422 186.777ZM286.262 254.795L176.072 223.99C176.072 223.99 177.265 230.038 181.842 237.869L274.617 263.805C282.255 259.386 286.262 254.795 286.262 254.795ZM209.867 321.102C122.618 297.71 133.166 186.543 147.284 133.865C153.097 112.156 159.073 96.0203 164.029 85.204C161.072 84.5953 158.623 86.1529 156.203 91.0746C150.941 101.747 144.212 119.124 137.7 143.45C123.586 196.127 113.038 307.29 200.283 330.682C241.406 341.699 273.442 324.955 297.323 298.659C274.655 319.19 245.714 330.701 209.867 321.102Z" fill="#2D4552"/>
|
||||
<path d="M161.661 262.296V239.863L99.3324 257.537C99.3324 257.537 103.938 230.777 136.444 221.556C146.302 218.762 154.713 218.781 161.661 220.123V128.11H192.869C189.471 117.61 186.184 109.526 183.423 103.909C178.856 94.612 174.174 100.775 163.545 109.665C156.059 115.919 137.139 129.261 108.668 136.933C80.1966 144.61 57.179 142.574 47.5752 140.911C33.9601 138.562 26.8387 135.572 27.5049 145.928C28.0847 155.062 30.2605 169.224 35.2445 187.928C46.0272 228.433 81.663 306.481 149.01 288.342C166.602 283.602 179.019 274.233 187.626 262.291H161.661V262.296ZM61.0848 188.484L108.946 175.876C108.946 175.876 107.551 194.288 89.6087 199.018C71.6614 203.743 61.0848 188.484 61.0848 188.484Z" fill="#E2574C"/>
|
||||
<path d="M341.786 129.174C329.345 131.355 299.498 134.072 262.612 124.185C225.716 114.304 201.236 97.0224 191.537 88.8994C177.788 77.3834 171.74 69.3802 165.788 81.4857C160.526 92.163 153.797 109.54 147.284 133.866C133.171 186.543 122.623 297.706 209.867 321.098C297.093 344.47 343.53 242.92 357.644 190.238C364.157 165.917 367.013 147.5 367.799 135.625C368.695 122.173 359.455 126.078 341.786 129.174ZM166.497 172.756C166.497 172.756 180.246 151.372 203.565 158C226.899 164.628 228.706 190.425 228.706 190.425L166.497 172.756ZM223.42 268.713C182.403 256.698 176.077 223.99 176.077 223.99L286.262 254.796C286.262 254.791 264.021 280.578 223.42 268.713ZM262.377 201.495C262.377 201.495 276.107 180.126 299.422 186.773C322.736 193.411 324.572 219.208 324.572 219.208L262.377 201.495Z" fill="#2EAD33"/>
|
||||
<path d="M139.88 246.04L99.3324 257.532C99.3324 257.532 103.737 232.44 133.607 222.496L110.647 136.33L108.663 136.933C80.1918 144.611 57.1742 142.574 47.5704 140.911C33.9554 138.563 26.834 135.572 27.5001 145.929C28.08 155.063 30.2557 169.224 35.2397 187.929C46.0225 228.433 81.6583 306.481 149.005 288.342L150.989 287.719L139.88 246.04ZM61.0848 188.485L108.946 175.876C108.946 175.876 107.551 194.288 89.6087 199.018C71.6615 203.743 61.0848 188.485 61.0848 188.485Z" fill="#D65348"/>
|
||||
<path d="M225.27 269.163L223.415 268.712C182.398 256.698 176.072 223.99 176.072 223.99L232.89 239.872L262.971 124.281L262.607 124.185C225.711 114.304 201.232 97.0224 191.532 88.8994C177.783 77.3834 171.735 69.3802 165.783 81.4857C160.526 92.163 153.797 109.54 147.284 133.866C133.171 186.543 122.623 297.706 209.867 321.097L211.655 321.5L225.27 269.163ZM166.497 172.756C166.497 172.756 180.246 151.372 203.565 158C226.899 164.628 228.706 190.425 228.706 190.425L166.497 172.756Z" fill="#1D8D22"/>
|
||||
<path d="M141.946 245.451L131.072 248.537C133.641 263.019 138.169 276.917 145.276 289.195C146.513 288.922 147.74 288.687 149 288.342C152.302 287.451 155.364 286.348 158.312 285.145C150.371 273.361 145.118 259.789 141.946 245.451ZM137.7 143.451C132.112 164.307 127.113 194.326 128.489 224.436C130.952 223.367 133.554 222.371 136.444 221.551L138.457 221.101C136.003 188.939 141.308 156.165 147.284 133.866C148.799 128.225 150.318 122.978 151.832 118.085C149.393 119.637 146.767 121.228 143.776 122.867C141.759 129.093 139.722 135.898 137.7 143.451Z" fill="#C04B41"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
@@ -0,0 +1,9 @@
|
||||
<svg width="48" height="48" viewBox="0 0 400 400" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M136.444 221.556C123.558 225.213 115.104 231.625 109.535 238.032C114.869 233.364 122.014 229.08 131.652 226.348C141.51 223.554 149.92 223.574 156.869 224.915V219.481C150.941 218.939 144.145 219.371 136.444 221.556ZM108.946 175.876L61.0895 188.484C61.0895 188.484 61.9617 189.716 63.5767 191.36L104.153 180.668C104.153 180.668 103.578 188.077 98.5847 194.705C108.03 187.559 108.946 175.876 108.946 175.876ZM149.005 288.347C81.6582 306.486 46.0272 228.438 35.2396 187.928C30.2556 169.229 28.0799 155.067 27.5 145.928C27.4377 144.979 27.4665 144.179 27.5336 143.446C24.04 143.657 22.3674 145.473 22.7077 150.721C23.2876 159.855 25.4633 174.016 30.4473 192.721C41.2301 233.225 76.8659 311.273 144.213 293.134C158.872 289.185 169.885 281.992 178.152 272.81C170.532 279.692 160.995 285.112 149.005 288.347ZM161.661 128.11V132.903H188.077C187.535 131.206 186.989 129.677 186.447 128.11H161.661Z" fill="#2D4552"/>
|
||||
<path d="M193.981 167.584C205.861 170.958 212.144 179.287 215.465 186.658L228.711 190.42C228.711 190.42 226.904 164.623 203.57 157.995C181.741 151.793 168.308 170.124 166.674 172.496C173.024 167.972 182.297 164.268 193.981 167.584ZM299.422 186.777C277.573 180.547 264.145 198.916 262.535 201.255C268.89 196.736 278.158 193.031 289.837 196.362C301.698 199.741 307.976 208.06 311.307 215.436L324.572 219.212C324.572 219.212 322.736 193.41 299.422 186.777ZM286.262 254.795L176.072 223.99C176.072 223.99 177.265 230.038 181.842 237.869L274.617 263.805C282.255 259.386 286.262 254.795 286.262 254.795ZM209.867 321.102C122.618 297.71 133.166 186.543 147.284 133.865C153.097 112.156 159.073 96.0203 164.029 85.204C161.072 84.5953 158.623 86.1529 156.203 91.0746C150.941 101.747 144.212 119.124 137.7 143.45C123.586 196.127 113.038 307.29 200.283 330.682C241.406 341.699 273.442 324.955 297.323 298.659C274.655 319.19 245.714 330.701 209.867 321.102Z" fill="#2D4552"/>
|
||||
<path d="M161.661 262.296V239.863L99.3324 257.537C99.3324 257.537 103.938 230.777 136.444 221.556C146.302 218.762 154.713 218.781 161.661 220.123V128.11H192.869C189.471 117.61 186.184 109.526 183.423 103.909C178.856 94.612 174.174 100.775 163.545 109.665C156.059 115.919 137.139 129.261 108.668 136.933C80.1966 144.61 57.179 142.574 47.5752 140.911C33.9601 138.562 26.8387 135.572 27.5049 145.928C28.0847 155.062 30.2605 169.224 35.2445 187.928C46.0272 228.433 81.663 306.481 149.01 288.342C166.602 283.602 179.019 274.233 187.626 262.291H161.661V262.296ZM61.0848 188.484L108.946 175.876C108.946 175.876 107.551 194.288 89.6087 199.018C71.6614 203.743 61.0848 188.484 61.0848 188.484Z" fill="#E2574C"/>
|
||||
<path d="M341.786 129.174C329.345 131.355 299.498 134.072 262.612 124.185C225.716 114.304 201.236 97.0224 191.537 88.8994C177.788 77.3834 171.74 69.3802 165.788 81.4857C160.526 92.163 153.797 109.54 147.284 133.866C133.171 186.543 122.623 297.706 209.867 321.098C297.093 344.47 343.53 242.92 357.644 190.238C364.157 165.917 367.013 147.5 367.799 135.625C368.695 122.173 359.455 126.078 341.786 129.174ZM166.497 172.756C166.497 172.756 180.246 151.372 203.565 158C226.899 164.628 228.706 190.425 228.706 190.425L166.497 172.756ZM223.42 268.713C182.403 256.698 176.077 223.99 176.077 223.99L286.262 254.796C286.262 254.791 264.021 280.578 223.42 268.713ZM262.377 201.495C262.377 201.495 276.107 180.126 299.422 186.773C322.736 193.411 324.572 219.208 324.572 219.208L262.377 201.495Z" fill="#2EAD33"/>
|
||||
<path d="M139.88 246.04L99.3324 257.532C99.3324 257.532 103.737 232.44 133.607 222.496L110.647 136.33L108.663 136.933C80.1918 144.611 57.1742 142.574 47.5704 140.911C33.9554 138.563 26.834 135.572 27.5001 145.929C28.08 155.063 30.2557 169.224 35.2397 187.929C46.0225 228.433 81.6583 306.481 149.005 288.342L150.989 287.719L139.88 246.04ZM61.0848 188.485L108.946 175.876C108.946 175.876 107.551 194.288 89.6087 199.018C71.6615 203.743 61.0848 188.485 61.0848 188.485Z" fill="#D65348"/>
|
||||
<path d="M225.27 269.163L223.415 268.712C182.398 256.698 176.072 223.99 176.072 223.99L232.89 239.872L262.971 124.281L262.607 124.185C225.711 114.304 201.232 97.0224 191.532 88.8994C177.783 77.3834 171.735 69.3802 165.783 81.4857C160.526 92.163 153.797 109.54 147.284 133.866C133.171 186.543 122.623 297.706 209.867 321.097L211.655 321.5L225.27 269.163ZM166.497 172.756C166.497 172.756 180.246 151.372 203.565 158C226.899 164.628 228.706 190.425 228.706 190.425L166.497 172.756Z" fill="#1D8D22"/>
|
||||
<path d="M141.946 245.451L131.072 248.537C133.641 263.019 138.169 276.917 145.276 289.195C146.513 288.922 147.74 288.687 149 288.342C152.302 287.451 155.364 286.348 158.312 285.145C150.371 273.361 145.118 259.789 141.946 245.451ZM137.7 143.451C132.112 164.307 127.113 194.326 128.489 224.436C130.952 223.367 133.554 222.371 136.444 221.551L138.457 221.101C136.003 188.939 141.308 156.165 147.284 133.866C148.799 128.225 150.318 122.978 151.832 118.085C149.393 119.637 146.767 121.228 143.776 122.867C141.759 129.093 139.722 135.898 137.7 143.451Z" fill="#C04B41"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
@@ -0,0 +1,33 @@
|
||||
// Skald connector entry point.
|
||||
//
|
||||
// The MCP server itself is the upstream `@playwright/mcp` package, pinned in
|
||||
// package.json and installed by Skald beside this file (`npm ci --omit=dev`,
|
||||
// falling back to `npm install --omit=dev`).
|
||||
//
|
||||
// This file exists because Skald launches a `mcp_local` connector as
|
||||
// `<command> <mcp_config.args[0]>`, where args[0] is rewritten to the absolute
|
||||
// path of a *shipped* file — so `npx @playwright/mcp` cannot be expressed:
|
||||
// args[0] would be taken as the entry file's name.
|
||||
//
|
||||
// The wrapper rewrites argv with the flags the server needs in a display-less
|
||||
// container (headless, isolated in-memory profile, no sandbox) and then
|
||||
// imports the package's CLI module, which self-executes at import time.
|
||||
// cli.js is not listed in the package's "exports" map, so it is resolved
|
||||
// from the exported package.json path instead of a package subpath import.
|
||||
import { createRequire } from "node:module";
|
||||
import { dirname, join } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const packageDir = dirname(require.resolve("@playwright/mcp/package.json"));
|
||||
|
||||
process.argv = [
|
||||
process.argv[0],
|
||||
"playwright-mcp",
|
||||
"--headless", // no display in the container/host session
|
||||
"--isolated", // in-memory profile: no shared cookies/login state on disk
|
||||
"--no-sandbox", // Chromium sandbox cannot run inside containers
|
||||
...process.argv.slice(2), // passthrough for any extra args
|
||||
];
|
||||
|
||||
await import(pathToFileURL(join(packageDir, "cli.js")));
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "skald-connector-playwright",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Skald connector wrapper around the @playwright/mcp MCP server.",
|
||||
"scripts": {
|
||||
"postinstall": "node node_modules/@playwright/mcp/cli.js install-browser chromium"
|
||||
},
|
||||
"dependencies": {
|
||||
"@playwright/mcp": "0.0.79"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Verify-before-save probe for the Playwright connector.
|
||||
//
|
||||
// Prints exactly ONE JSON object on stdout:
|
||||
// {"ok": bool, "message": string, "details"?: object}
|
||||
// Exit code 0 on success, 1 on failure. All diagnostics go to stderr.
|
||||
//
|
||||
// This connector has no credentials; what can actually break at runtime is
|
||||
// the Chromium side: the binary must be present and launchable headless in
|
||||
// this environment (system libraries, sandbox restrictions). The probe
|
||||
// launches a real headless Chromium on about:blank and closes it.
|
||||
import { createRequire } from "node:module";
|
||||
import { existsSync } from "node:fs";
|
||||
import { basename } from "node:path";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
const finish = (ok, message, details) => {
|
||||
const out = { ok, message };
|
||||
if (details) out.details = details;
|
||||
process.stdout.write(JSON.stringify(out) + "\n");
|
||||
process.exit(ok ? 0 : 1);
|
||||
};
|
||||
|
||||
let chromium;
|
||||
let executablePath;
|
||||
try {
|
||||
// Resolve playwright-core relative to @playwright/mcp (it is a dependency
|
||||
// of the pinned package, not of this connector's package.json).
|
||||
const mcpRequire = createRequire(require.resolve("@playwright/mcp/package.json"));
|
||||
({ chromium } = mcpRequire("playwright-core"));
|
||||
executablePath = chromium.executablePath();
|
||||
} catch (e) {
|
||||
finish(false, `Cannot resolve the Playwright installation: ${e.message}`);
|
||||
}
|
||||
|
||||
if (!existsSync(executablePath)) {
|
||||
finish(false, "The Chromium binary is not installed. Reinstall the connector dependencies (the postinstall step downloads it).", { executablePath });
|
||||
}
|
||||
|
||||
let browser;
|
||||
try {
|
||||
browser = await chromium.launch({ headless: true, args: ["--no-sandbox"] });
|
||||
} catch (e) {
|
||||
finish(false, `Chromium failed to launch: ${String(e.message).split("\n")[0]}`, { executablePath });
|
||||
}
|
||||
|
||||
try {
|
||||
const page = await browser.newPage();
|
||||
await page.goto("about:blank");
|
||||
finish(true, `Chromium launched headless successfully (${basename(executablePath)}).`);
|
||||
} finally {
|
||||
await browser.close().catch(() => {});
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
# Bug report: verify dei connector MCP in container fallisce con `exec: "-e": executable file not found`
|
||||
|
||||
**Progetto**: skald-circle (non il marketplace)
|
||||
**File**: `crates/skald-core/src/mcp/verify.rs`
|
||||
**Introdotto da**: commit `e6c4e20` — "feat(mcp): OAuth per-user connectors (§15) — providers, PKCE copy-paste flow, env credential delivery"
|
||||
**Priorità**: alta — blocca l'attivazione di tutti i connector `mcp_local` + `scope: user` che hanno env/secret nel verify (LinkedIn ora, Email alla prossima riattivazione)
|
||||
|
||||
---
|
||||
|
||||
## 1. Sintomo
|
||||
|
||||
Attivando il connector `linkedin` (mcp_local, scope user) dal frontend, il verify fallisce con:
|
||||
|
||||
```
|
||||
Failed — OCI runtime exec failed: exec failed: unable to start container process: exec: "-e": executable file not found in $PATH
|
||||
```
|
||||
|
||||
## 2. Causa radice
|
||||
|
||||
In `crates/skald-core/src/mcp/verify.rs` (~righe 144-152) il comando `docker exec` viene costruito con il **nome del container PRIMA delle opzioni `-e`**:
|
||||
|
||||
```rust
|
||||
VerifyTarget::Container { container, workdir } => {
|
||||
let mut c = tokio::process::Command::new("docker");
|
||||
c.arg("exec")
|
||||
.arg("-w").arg(workdir)
|
||||
.arg(container); // ← container name
|
||||
inject_env_flags(&mut c, env_values, secret_values); // ← -e KEY=VAL DOPO il container
|
||||
c.arg("sh").arg("-c").arg(&resolved);
|
||||
c
|
||||
}
|
||||
```
|
||||
|
||||
`inject_env_flags` (righe 285-293) aggiunge `cmd.arg("-e").arg(format!("{k}={v}"))` per ogni env/secret.
|
||||
|
||||
Risultato: viene generato
|
||||
|
||||
```
|
||||
docker exec -w <workdir> <container> -e KEY=VAL -e KEY2=VAL2 sh -c "python3 verify.py"
|
||||
```
|
||||
|
||||
La sintassi di `docker exec` è `docker exec [OPTIONS] CONTAINER COMMAND [ARG...]`: **dopo** il nome del container ogni argomento è interpretato come COMMAND. Docker prova quindi a eseguire l'eseguibile `-e` → errore OCI.
|
||||
|
||||
## 3. Connector colpiti
|
||||
|
||||
Il bug scatta solo quando `env_values`/`secret_values` non sono vuoti, cioè quando il manifest del connector dichiara `env[]` (o secret) e il verify gira **in container**:
|
||||
|
||||
| Connector | Container? | Verify con env? | Colpito |
|
||||
|---|---|---|---|
|
||||
| linkedin | ✅ user | ✅ 2 secret + 3 env | ❌ si — errore visibile |
|
||||
| email | ✅ user | ✅ 8 env | ❌ si — già attivo, fallirà alla prossima riattivazione |
|
||||
| gcal / drive / gmail | ✅ user | credenziali OAuth via altro path | probabilmente no |
|
||||
| gmaps, context7, tavily… | host / global | VerifyTarget::Host | no |
|
||||
|
||||
Nota: il path di **lancio del server MCP** in `crates/mcp-client/src/server.rs` (~righe 261-273) costruisce lo stesso comando **nell'ordine corretto** (`-i`, poi `-e …`, poi container) — usarlo come riferimento.
|
||||
|
||||
## 4. Fix suggerito
|
||||
|
||||
Spostare `inject_env_flags` **prima** di `.arg(container)` in `verify.rs`:
|
||||
|
||||
```rust
|
||||
VerifyTarget::Container { container, workdir } => {
|
||||
let mut c = tokio::process::Command::new("docker");
|
||||
c.arg("exec").arg("-w").arg(workdir);
|
||||
inject_env_flags(&mut c, env_values, secret_values); // ← spostato qui
|
||||
c.arg(container);
|
||||
c.arg("sh").arg("-c").arg(&resolved);
|
||||
c
|
||||
}
|
||||
```
|
||||
|
||||
Il comando generato diventa:
|
||||
|
||||
```
|
||||
docker exec -w <workdir> -e KEY=VAL -e KEY2=VAL2 <container> sh -c "python3 verify.py"
|
||||
```
|
||||
|
||||
## 5. Verifica consigliata
|
||||
|
||||
1. **Test unitario**: aggiungere un test che costruisca il comando per `VerifyTarget::Container` e verifichi l'ordine degli argomenti (`docker exec -w <wd> -e K=V <container> sh -c …`). I test esistenti in `verify.rs` coprono solo `apply_placeholders`.
|
||||
2. **E2E**: riattivare `linkedin` dal frontend (deve superare il verify con il cookie li_at).
|
||||
3. **Regressione**: riattivare `email` (8 env) per confermare che il bug era latente anche lì.
|
||||
Reference in New Issue
Block a user