Publish an Ability to the Vetta Marketplace
Submitting is a local action: it reads an archive from the user's disk, so it runs through the bundled script rather than a tool call.
bashnode "$SKILL_DIR/scripts/publish.mjs" --input /abs/path/to/payload.json
Checking review status afterwards is a remote action — use the list_my_abilities tool from
the built-in vetta MCP server. It is always available and needs no setup.
Procedure
-
Determine the type. Ask the user if it is not obvious from the artifact.
Type What it is Physical artifact skillA SKILL.mdworkflow.zip/.tar.gzcontainingSKILL.mdsceneA skill installed into the scene directory same as skillpluginA Vetta plugin .zipcontainingplugin.jsonmcpOne entry in the user's mcp.jsonnone — config only bundleA named group of already-published abilities none — references only -
Source
detailfrom the package. Do not invent it.name,description,author, andcontentare all required and the submission is rejected without them. Readplugin.json(name/description/author),SKILL.mdfrontmatter,README.md,LICENSE. Ask the user for anything you cannot source.For plugins, also list
locales/*.json: those filenames are the exact locale keys yourdetail.i18nmust use, and whatever they already translate is text you should not restate. See Multi-language.contentis the detail-page body in markdown — it is what a user reads to decide whether to install. Write what the ability does, when to use it, and any setup it needs. A one-linecontentis not acceptable. -
Read the available categories before you set one.
categoryis matched by name — a name that matches nothing is not an error, it silently lands in "uncategorised". So never invent one:bashnode "$SKILL_DIR/scripts/categories.mjs" # human-readable node "$SKILL_DIR/scripts/categories.mjs" --json # machine-readablePut the
namefield verbatim into the payload'scategory. Thei18nvalues are display translations only — matching accepts them, but the canonical name is what you should write. Omitcategoryentirely if nothing fits. -
Write the payload to a JSON file, then pass it with
--input. Never try to pass fields as command-line arguments:contentis multi-line markdown containing backticks, quotes and$, and the shell will mangle it.Full field reference: references/payload.md — read it when you need anything beyond the four required
detailfields. It is also where the valid icon names and the meaning of eachshowcase.canvasvalue are listed; both are closed sets, and a wrong value either fails validation or renders as a fallback. -
Dry-run first when the payload is non-trivial. It runs the full validation without submitting, so you spend no network round-trip on a payload that was never going to pass:
bashnode "$SKILL_DIR/scripts/publish.mjs" --input payload.json --dry-runFor
skill/scene/pluginthis also opens the archive and cross-checks the payload againstplugin.json,locales/*.json, andSKILL.mdfrontmatter. That is the only place the "same data, two sources" mistakes surface: a translation key that does not match the package's own locale files, aslugorversionthat the server will ignore, avetta.jsonthat yourdetailsilently supersedes. None of these fail the upload — they just make part of what you wrote unreachable. Do not skip the dry-run on packaged types. -
Submit. On failure the script returns all problems at once — fix the whole list before retrying rather than resubmitting after each single fix.
Script contract
- stdout is a single JSON object; exit code 0 = success, 1 = failure.
- Failure shape:
{"ok": false, "message": "...", "errors": [...]}—errorsis present for validation failures. - Success shape:
{"ok": true, "message": "...", "slug": "...", "version": "...", "review_status": "...", "has_pending": bool, "warnings": [...]}. warningsare things that did not block the submission but changed what gets published — a hand-written translation overriding the package's own, aslugthat was ignored, avetta.jsonthat was skipped. Relay them to the user; do not treat the run as clean.- Input may also arrive on stdin (
cat payload.json | node .../publish.mjs) if that is more convenient than a temp file. - The script reads the login token from
~/.vetta/auth.jsonitself. If it reports "未登录", tell the user to log in through the Vetta client — do not attempt to pass credentials yourself.
Review
Submissions from non-administrators enter a review queue and are not visible in the marketplace until an administrator approves them. Tell the user this rather than implying the ability is live.
Re-submitting an ability that is already published does not disturb it: the new version waits in
a pending slot while the marketplace keeps serving the current version, so installed users are
unaffected until the update is approved. has_pending: true in the result means exactly this.
A re-submission replaces detail.i18n wholesale. Sending only the locale you changed drops
every other translation on that entry, including any an administrator added later. Fetch the
current state first (list_my_abilities) or resend the complete set of locales.
You may only submit updates to abilities you own. Submitting to a slug owned by someone else is rejected.
Verify
After a successful run, state the returned slug, version, and whether the ability is live or
pending review. If the run failed, fix every listed error and retry — do not report success.
Use list_my_abilities to report progress later: review_status is pending, approved, or
rejected (reason in review_note); a non-empty pending_version means an update is queued
behind the live version.


