MCP Registry
Scanning tells you which MCP servers are bad; the MCP Registry puts the
good ones on developer machines. The org publishes a reviewed catalog, and
ringzero-mcp registry sync projects it into every supported harness’s
config file — Claude Code, VS Code, Cursor, OpenCode, and (machine scope)
Claude Desktop. Orgs control the offering and the security posture;
developers get working agents by pulling; internal servers join through the
same PR review as any code change.
The catalog file
Section titled “The catalog file”.ringzero/mcp-registry.json, committed at the repo root (resolution:
--registry flag > RINGZERO_MCP_REGISTRY env > walk-up from cwd — point
the env at a shared path to distribute one catalog across many repos):
{ "schemaVersion": 1, "organization": "org_9f3k", "servers": [ { "name": "github", "description": "Official GitHub MCP server", "owner": "platform-team", "transport": "stdio", "command": "npx", "args": ["-y", "@modelcontextprotocol/server-github@2025.4.8"], "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "${env:GITHUB_TOKEN}" }, "harnesses": "all", "scope": "project" }, { "name": "linear", "transport": "http", "url": "https://mcp.linear.app/sse", "headers": { "Authorization": "${env:LINEAR_MCP_KEY}" }, "harnesses": ["claude-code", "opencode"] } ]}| Field | Meaning |
|---|---|
controlLevel | Optional top-level org control level (open, monitored, strict) — see Org control levels |
name | Unique config key ([A-Za-z0-9._-]+) |
transport | stdio (command/args/env) or http (url/headers) |
harnesses | "all" or a subset of claude-code, vscode, cursor, opencode, claude-desktop |
scope | project (repo configs, default) or machine (user-level configs; required for claude-desktop) |
enabled | false removes the server from managed configs — the rollout kill switch |
owner, description | Attribution — recommended for dev-provided internal servers |
Validation is the security gate. ringzero-mcp registry validate rejects:
unknown keys, duplicate names, unpinned stdio sources (same rules as the
scanner’s Unpinned version check),
literal env/header values, and secrets smuggled into args or URLs. An org
cannot ship a bad catalog.
How sync works
Section titled “How sync works”ringzero-mcp registry sync compares the catalog projection against each
target config and applies the difference:
- add — catalog entry missing from the config.
- update — a managed entry differs from the catalog (the registry wins for entries it owns).
- remove — a managed entry left the catalog or was
enabled: false. - conflict — an entry you created yourself has the same name as a catalog entry: sync never touches it and reports the conflict until a human resolves it.
Ownership lives in a lock file — .ringzero/mcp-registry.lock.json
(committed) for project scope, ~/.ringzero/mcp-registry-state.json for
machine scope. Entries the registry didn’t create are never modified or
removed, and unrelated keys in the config files are always preserved.
Sync is idempotent: only drifted files are written, so clean files stay
byte-identical. One caveat: the first managed write to a .jsonc file
(.vscode/mcp.json, opencode.jsonc) rewrites it as plain JSON — comments
are lost on that first change.
What gets written where
Section titled “What gets written where”Project scope writes exactly one config per harness at the catalog root
(nested configs are scanned, never managed); --machine writes user-level
configs:
| Harness | Project scope | Machine scope (--machine) |
|---|---|---|
| Claude Code | .mcp.json | ~/.claude.json (top-level servers only) |
| VS Code | .vscode/mcp.json | — |
| Cursor | .cursor/mcp.json | ~/.cursor/mcp.json |
| OpenCode | opencode.json[c] | ~/.config/opencode/opencode.json |
| Claude Desktop | — | claude_desktop_config.json (stdio entries only) |
Registry entries are approved
Section titled “Registry entries are approved”A server matching a catalog entry (name + source) is automatically
approved by the scanner’s Unapproved server check — the catalog is the
allowlist for everything it manages, with no duplicated
policy entries. The policy still governs everything
else: runtime dangerous patterns, non-registry servers, and enforce-mode
gating.
Adding an internal server
Section titled “Adding an internal server”Developers propose servers with one command and a PR:
-
Scaffold the entry — from flags, or from a server already configured locally (
--fromnever copies literal env values; every env key becomes a${env:KEY}reference):Terminal window ringzero-mcp registry add docs-search \--command npx --arg=-y --arg @acme-internal/docs-search-mcp@1.2.0 \--env 'DOCS_TOKEN=${env:DOCS_TOKEN}' \--owner devex --description "Internal docs search" -
The entry is validated on the spot (pinning, references, shape) and appended to
.ringzero/mcp-registry.json. -
Commit and open a PR — review is the approval step. On merge, every deployment channel rolls the server out org-wide.
Command reference
Section titled “Command reference”| Command | Does | Exit |
|---|---|---|
registry sync [path] | Apply the catalog (--check --machine --json --quiet --registry <f>) | 0 · 1 on drift with --check · 2 |
registry status [path] | Drift report, writes nothing | always 0 |
registry list | Print catalog entries (--json) | 0 |
registry validate | Lint the catalog | 0 valid · 1 invalid · 2 none found |
registry add <name> | Propose an entry (--from <server> or flags) | 0 · 1 · 2 |
registry remove <name> | Drop an entry (next sync removes it from configs) | 0 · 1 · 2 |
registry pull | Fetch the org catalog from RingZero | 0 · 1 · 2 |
No catalog found → sync and status are silent exit-0 no-ops, so
hooks are safe in repos that haven’t adopted the registry.
Manage in the app & via the API
Section titled “Manage in the app & via the API”The RingZero app manages the same catalog shape centrally on the
MCP registry page (/mcp/registry). Catalog management is role-gated to
Admins/Owners and session-only — no API-key scope covers it (a key
gets 403 insufficient_scope); keys only read the catalog.
PUT /v1/mcp/registry/entriesSession-only (Admin/Owner). Replaces the org catalog’s servers
collection in full — the body is the entries array in the exact
catalog entry shape:
{ "entries": [ { "name": "github", "transport": "stdio", "command": "npx", "args": ["-y", "@modelcontextprotocol/server-github@2025.4.8"], "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "${env:GITHUB_TOKEN}" } } ]}Every entry passes the same validation gates as registry validate —
pinned stdio sources, ${env:VAR}-only env/header values, unique names, no
unknown keys, and typed/bounded string fields. Any failure is
422 invalid_entry with the message naming the failing index
(entries[1]: unpinned source — …), and nothing is stored:
registry pull rejects a whole catalog on any invalid entry, so one bad row
would brick pull for the entire org. The org’s
control level is a separate setting with its
own Owner-only writer — this request never reads or writes it. The response
is the stored collection, { "entries": [...] }, and GET /v1/mcp/registry
serves it immediately. Each replace is recorded in the org audit trail as
mcp_registry_entries_replaced (entry counts plus the added/removed entry
names — never entry bodies).
Team enablement (new teams inherit all-targeted entries), per-repository
overrides (inherit | force on | force off) with an effective-catalog
preview, and staged rollouts Coming soon
build on the same shape. Assignment data lives in the platform, never in
the catalog file, so the file format you commit today doesn’t change.
The org control level API
Section titled “The org control level API”GET /v1/mcp/control-levelPUT /v1/mcp/control-levelThe org-wide control level
distributed via the catalog. The read requires scope
mcp:read and answers
{ "controlLevel": "open" | "monitored" | "strict" } — monitored when the
org has never set one. The write is Owner-only and session-only (no
API-key scope covers it): send { "controlLevel": "strict" }, get back the
new { "controlLevel" }; any other value is 422 invalid_control_level.
Catalog entries are untouched by a level change, and an actual change is
recorded in the org audit trail as mcp_control_level_changed (old and new
value, acting user, timestamp).
Platform sync
Section titled “Platform sync”GET /v1/mcp/registry (scope mcp:read) serves the org catalog centrally,
and ringzero-mcp registry pull is the shipped client — it validates the
payload before overwriting the local file, and without a RINGZERO_API_KEY
it explains and exits 0. An org that hasn’t published a catalog yet gets a
valid empty one (with the default monitored control level) rather than an
error — but think before scripting pull across repos that haven’t adopted
the registry: the first pull materializes an explicit local catalog whose
controlLevel: monitored outranks any policy-file mode in the
runtime precedence chain, and
an empty servers list approves nothing. Repos meant to stay
policy-file-only should simply not pull. Per-team/per-repo effective
catalogs (?team= / ?repo=) Coming soon
build on the same shape.