Skip to content

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.

.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):

.ringzero/mcp-registry.json
{
"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"]
}
]
}
FieldMeaning
controlLevelOptional top-level org control level (open, monitored, strict) — see Org control levels
nameUnique config key ([A-Za-z0-9._-]+)
transportstdio (command/args/env) or http (url/headers)
harnesses"all" or a subset of claude-code, vscode, cursor, opencode, claude-desktop
scopeproject (repo configs, default) or machine (user-level configs; required for claude-desktop)
enabledfalse removes the server from managed configs — the rollout kill switch
owner, descriptionAttribution — 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.

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.

Project scope writes exactly one config per harness at the catalog root (nested configs are scanned, never managed); --machine writes user-level configs:

HarnessProject scopeMachine scope (--machine)
Claude Code.mcp.json~/.claude.json (top-level servers only)
VS Code.vscode/mcp.json
Cursor.cursor/mcp.json~/.cursor/mcp.json
OpenCodeopencode.json[c]~/.config/opencode/opencode.json
Claude Desktopclaude_desktop_config.json (stdio entries only)

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.

Developers propose servers with one command and a PR:

  1. Scaffold the entry — from flags, or from a server already configured locally (--from never 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"
  2. The entry is validated on the spot (pinning, references, shape) and appended to .ringzero/mcp-registry.json.

  3. Commit and open a PR — review is the approval step. On merge, every deployment channel rolls the server out org-wide.

CommandDoesExit
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 nothingalways 0
registry listPrint catalog entries (--json)0
registry validateLint the catalog0 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 pullFetch the org catalog from RingZero0 · 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.

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/entries

Session-only (Admin/Owner). Replaces the org catalog’s servers collection in full — the body is the entries array in the exact catalog entry shape:

request body
{
"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.

GET /v1/mcp/control-level
PUT /v1/mcp/control-level

The 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).

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.

Next: deploy the registry to your org.