Publish an MCP server
An mcp entry ships a Model Context Protocol server: the catalog stores a
declaration; the assistant runs it. This page is the contract for the mcp
nature: the catalog entry it needs, the one strict rule its secret
references obey, and what an install actually does with it.
It assumes you already have a catalog repository and know the edit-install-tag loop. If you do not,
build one first in create a catalog; that tutorial owns the general
mechanics (git repo, catalog.json skeleton, cutting a version). Here you only add one mcp entry to
a catalog that already exists.
The catalog entry
Section titled “The catalog entry”An mcp server is an artifact entry with nature: "mcp". Unlike a
skill, it carries no folder: the whole server declaration lives inline in the entry, under config.
A GitHub MCP server for Claude Code looks like this:
{ "kind": "artifact", "id": "mcp:github", "nature": "mcp", "targets": ["claude"], "scopes": ["user"], "config": { "command": "docker", "args": [ "run", "-i", "--rm", "-e", "GITHUB_PERSONAL_ACCESS_TOKEN", "ghcr.io/github/github-mcp-server" ], "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_PERSONAL_ACCESS_TOKEN}" } }, "secrets": [ { "ref": "GITHUB_PERSONAL_ACCESS_TOKEN", "prompt": "GitHub personal access token", "required": true, "help": "https://github.com/settings/tokens" } ]}Beyond the fields every artifact shares, an mcp entry adds config and secrets:
| Field | Required | For an mcp entry |
|---|---|---|
kind |
yes | Always "artifact". An mcp server is a single artifact, never a pack. |
id |
yes | mcp:<name>. The <name> after the prefix is the server id the assistant stores it under. |
nature |
yes | "mcp". |
targets |
yes | The assistants that get it: claude, opencode, or both. |
scopes |
yes | user, project, or both. See scope. |
config |
yes¹ | The server declaration, passed through verbatim. Shape depends on the assistant — see transport. |
secrets |
no | One declaration per ${VAR} reference the config carries. See the secret constraint. |
requires |
no | Ids of entries that must install first. See requires. |
¹ The parser accepts an mcp entry with no config, but an install then has nothing to add; the
adapter, not the schema, enforces that a real server has one. The full field-by-field rules are in
the catalog.json schema.
The transport
Section titled “The transport”Because config is handed to the assistant untouched, its shape belongs to the assistant, not to
agent-rigger. The two assistants declare a server differently:
- Claude Code takes a native descriptor. The stdio form above uses
command,args, and anenvmap. A remote server would carry aurlandheadersinstead. - opencode takes a discriminated shape under
config. A local (spawned) server is{ "type": "local", "command": [...], "environment": { ... } }; a remote one is{ "type": "remote", "url": "...", "headers": { ... } }.
The same GitHub server, targeted at opencode as a local server, is:
{ "kind": "artifact", "id": "mcp:github", "nature": "mcp", "targets": ["opencode"], "scopes": ["user"], "config": { "type": "local", "command": [ "docker", "run", "-i", "--rm", "-e", "GITHUB_PERSONAL_ACCESS_TOKEN", "ghcr.io/github/github-mcp-server" ], "environment": { "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_PERSONAL_ACCESS_TOKEN}" } }, "secrets": [ { "ref": "GITHUB_PERSONAL_ACCESS_TOKEN", "prompt": "GitHub personal access token", "required": true, "help": "https://github.com/settings/tokens" } ]}The secret constraint
Section titled “The secret constraint”A server needs a token, and that token must never be committed. agent-rigger keeps the value out of
the catalog entirely: config carries a
${VAR} reference, and the value is
supplied on the installing machine at install time. Three sub-objects are the ones that may hold a
secret, and every value in them must be an exact ${VAR_NAME} reference: env (Claude Code’s stdio
field), and environment and headers (opencode’s fields). Keys outside those three (command,
args, url, type) carry no such rule.
The match is strict. A literal value, or a partial reference like "Bearer ${TOKEN}", is rejected
when the catalog parses, before any install work runs. Write a literal:
"env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_a_real_token" }and the parse fails, naming the entry and the offending path:
[error] catalog.json contains invalid entries: index 0: config.env.GITHUB_PERSONAL_ACCESS_TOKEN mcp entry "mcp:github" has a non-ref value at config.env.GITHUB_PERSONAL_ACCESS_TOKEN — use a "${VAR_NAME}" reference instead of a literal valueEach reference the config carries gets one entry in secrets. A declaration names the reference and
tells the installer how to ask for it:
| Field | Required | For a secret declaration |
|---|---|---|
ref |
yes | The name inside ${…} — here GITHUB_PERSONAL_ACCESS_TOKEN. Ties the declaration to the config. |
prompt |
yes | The label shown when the installer asks which environment variable holds the value. |
required |
no | When true, install fails closed if the secret is never resolved (see the loop). |
example |
no | Advisory example of the value’s format. Never a real secret. |
help |
no | Advisory text or URL, for example where to generate the token. |
No value ever appears anywhere in the entry: secrets declares reference names and advisory text
only. How a reference is mapped to a real variable at install time, and where the value does and does
not land, is give an MCP server its secret.
What an install produces
Section titled “What an install produces”The two assistants use unrelated mechanisms, and neither ever writes a secret value. For claude,
install delegates to Claude Code’s own CLI:
claude mcp add-json <server> <json> -s <scope>. The config it passes still carries a ${VAR}
reference, but agent-rigger rewrites the name inside ${…} first: it becomes whichever variable
--secret-env mapped the catalog’s ref to, or the ref’s own name when no mapping was given. That
rewritten name is what Claude Code stores in its own config, on this machine, and what it expands
when it later launches the server; how the mapping itself resolves is
give an MCP server its secret. For opencode, install merges the server
under the mcp key of opencode.json at server granularity, preserving every other key and any
server already there. The exact path, config file, and mechanism per assistant and scope are the
mcp row of the natures matrix.
Test it before you tag
Section titled “Test it before you tag”Prove the entry parses and installs before you cut a release. Point the tool at a throwaway home and
install your catalog folder straight from disk: install reads a local path directly as an
ad-hoc source, no catalog add step needed.
A claude-target mcp entry has a second isolation concern a skill does not. Its install delegates the
write to Claude Code’s own CLI, which writes into Claude Code’s config, not under
RIGGER_HOME. Point that config at the sandbox too with
CLAUDE_CONFIG_DIR, or the server lands in your real ~/.claude.json:
export RIGGER_HOME="$(mktemp -d)"export CLAUDE_CONFIG_DIR="$RIGGER_HOME/.claude-cfg"export NO_COLOR=1install clones whatever is at HEAD, never your working tree, so commit the entry (the
catalog.json change) before installing, or the plan silently omits it: no error, exit 0, the rest
of the catalog installs as if the new server were never added.
Then map the reference to a variable and install by path. In a non-interactive shell --yes is
required and selects every entry the source offers; a secret marked required has no default there,
so pass --secret-env too:
export MY_GH_PAT=ghp_your_tokenrigger install /path/to/your-catalog --secret-env=GITHUB_PERSONAL_ACCESS_TOKEN=MY_GH_PAT --yes--- Plan ---Plan · 1 change · scope: user (~/.claude)
+ local-your-catalog/mcp:github
--- Result --- [ok] Applied 0 file(s).The Plan previews one change; Result reports Applied 0 file(s) because agent-rigger wrote
no file of its own; Claude Code’s CLI added the server, not agent-rigger’s file writer. The
local-your-catalog/ prefix is
provenance: an ad-hoc install derives an id prefix
from the source rather than a registered catalog name.
Skip the flag and the required secret fails closed, before anything is written:
[error] missing required secret "GITHUB_PERSONAL_ACCESS_TOKEN" (GitHub personal access token) — pass --secret-env=GITHUB_PERSONAL_ACCESS_TOKEN=<VAR_NAME> or export GITHUB_PERSONAL_ACCESS_TOKEN directlyThe mapping itself (how --secret-env resolves, what an interactive session asks, and where the
value goes) is give an MCP server its secret. Erase the sandbox when done:
rm -rf "$RIGGER_HOME"unset RIGGER_HOME CLAUDE_CONFIG_DIR MY_GH_PATOnce the entry installs clean, commit it and cut a version the way create a catalog shows. An mcp server is only real for your team once it is a tagged release.
Other natures
Section titled “Other natures”This page covers the mcp nature only. Each of the eight natures has its own on-disk contract; the
complete map, per assistant and per scope, is the natures matrix. The
last one has its own page too: declare a tool dependency covers tool’s
presence check, which works today, though its install from the package-manager hints is not yet
delivered.