docs/registries.md
mcpproxy discovers MCP servers through a built-in set of registries. Discovery is
available via the search_servers / list_registries MCP tools, the
mcpproxy registry search|list|add CLI, and the REST /api/v1/registries routes.
| ID | Name | Protocol | Key required | Notes |
|---|---|---|---|---|
official | Official MCP Registry | modelcontextprotocol/registry | no | Primary, zero-config aggregator (registry.modelcontextprotocol.io/v0.1/servers). |
reference | Reference Servers | builtin/reference | no | Curated @modelcontextprotocol servers, shipped in-binary so the basics work offline. |
docker-mcp-catalog | Docker MCP Catalog | custom/docker | no | Signed-container MCP server inventory. |
The shipped default set is exactly these three official, built-in entries. Earlier
versions also shipped pulse, smithery, fleur, azure-mcp-demo, and
remote-mcp-servers as defaults; these were removed. They are pruned from an
existing mcp_config.json on load (genuinely user-added custom registries are never
touched), so upgrading installs converge to the three above. pulse and smithery
can still be added back as custom sources (see Adding your own registry source);
when added they read MCPPROXY_REGISTRY_PULSE_API_KEY / MCPPROXY_REGISTRY_SMITHERY_API_KEY.
Key-requiring registries are skipped (not failed) when no key is configured, so
a default search always succeeds. The API-key env var is
MCPPROXY_REGISTRY_<ID>_API_KEY (ID upper-cased, non-alphanumerics → _). When a
key is configured it is sent on every request to that registry as an
Authorization: Bearer <key> header.
User-configured registries in mcp_config.json (registries: [...]) are merged
with these defaults (keyed by ID); a custom entry never drops the shipped set.
Every registry carries a provenance tag:
| Provenance | Meaning |
|---|---|
official | A shipped, built-in default (the three above). |
custom | Any registry the user added at runtime, or any non-default ID in mcp_config.json. |
Trust is derived, not asserted — it comes solely from whether the registry ID
is one of the shipped defaults. Writing "provenance": "official" into a
custom mcp_config.json entry has no effect; mcpproxy recomputes provenance on
every merge. There is no allowlist a user can add themselves into.
Provenance is informational only (it no longer changes quarantine behavior):
skip_quarantine. A server's origin is still
recorded on its config as source_registry_id / source_registry_provenance
and surfaced in the approval/quarantine view.list_registries output (MCP, REST, CLI) includes provenance and a
trusted boolean (derived official == trusted) so a UI can show a neutral
Official / Custom badge.official/trusted /
custom/unverified; these are normalized to official / custom on read, so
an existing config.db / mcp_config.json keeps working unchanged.mcpproxy registry add-source adds any https endpoint that implements the official
modelcontextprotocol/registry v0.1 protocol (the same protocol Copilot / VS Code /
Azure ship):
mcpproxy registry add-source https://registry.example.com
mcpproxy registry add-source https://registry.example.com --id acme --name "Acme Corp"
The ID is derived from the host when omitted; --protocol defaults to
modelcontextprotocol/registry. The source is always tagged custom.
This requires a running daemon — the registry list is updated copy-on-write on the
runtime config snapshot and persisted to mcp_config.json.
Equivalent surfaces:
POST /api/v1/registries with { "url": "https://…", "protocol": "…", "id": "…", "name": "…" }.mcpproxy registry add-source <https-url>.Errors share a stable code across surfaces: invalid_registry_url (400),
registries_locked (403), registry_shadows_builtin / duplicate_registry (409).
The Web UI maps each code to an actionable message.
mcpproxy registry remove <id> deletes a custom registry you added earlier. Only
custom registries can be removed — the shipped built-in defaults are
refused via the same shadow guard as add-source. Removing a source does not touch
any upstream servers you already added from it.
mcpproxy registry list # find the id
mcpproxy registry remove acme # delete the custom source (aliases: rm, remove-source)
Like add-source, this requires a running daemon — the change is applied
copy-on-write on the runtime config snapshot and persisted to mcp_config.json.
Equivalent surfaces:
DELETE /api/v1/registries/{id} → { "registry": { … } } echoing the removed entry.mcpproxy registry remove <id>.Errors share a stable code across surfaces: registry_not_found (404),
registry_shadows_builtin (409, built-in cannot be removed),
registries_locked (403).
mcpproxy registry edit <id> updates a custom registry you added earlier — its
display name, base URL, or servers-collection URL. Only custom registries can be
edited; the shipped built-in defaults are refused via the same shadow guard as
add/remove-source. Omitted flags leave the existing value unchanged. Changing
--url re-derives the servers URL unless --servers-url is also given.
mcpproxy registry edit acme --url https://new.acme.example.com # change the URL
mcpproxy registry edit acme --name "Acme Corp" # change the display name
Like add/remove-source, this requires a running daemon — the change is applied
copy-on-write on the runtime config snapshot and persisted to mcp_config.json.
Equivalent surfaces:
PUT /api/v1/registries/{id} with { "name": "…", "url": "https://…", "servers_url": "https://…" } (all optional) → { "registry": { … } } echoing the updated entry.mcpproxy registry edit <id> [--name … --url … --servers-url …].Errors share a stable code across surfaces: registry_not_found (404),
registry_shadows_builtin (409, built-in cannot be edited),
invalid_registry_url (400), registries_locked (403).
registries_locked (stub)Setting "registries_locked": true in mcp_config.json disables runtime registry
changes (registry add-source / registry remove and the REST add-source and
remove surfaces return registries_locked). Built-in defaults are unaffected.
This is a forward-looking stub for enterprise policy pinning.
The official registry returns a cursor-paginated list of wrapped entries:
{ "servers": [ { "server": { /* server.json */ }, "_meta": { "io.modelcontextprotocol.registry/official": { "status": "active", "isLatest": true } } } ],
"metadata": { "nextCursor": "..." } }
mcpproxy:
.server, and skips entries whose _meta status is
deleted/deprecated or that are not isLatest;metadata.nextCursor (bounded) and passes through version=latest and an
optional search query.Classification is per transport entry — never "has remotes ⇒ remote" (the fix for GH #567 / #483):
| server.json | Result |
|---|---|
packages[] present | stdio: launch command derived from runtimeHint + runtimeArguments + identifier(@version) + packageArguments; environmentVariables[] become required inputs. No URL. |
remotes[] only | remote/http: type + url become the connection endpoint; headers[] become required inputs. |
| both (hybrid) | the package is preferred (stdio); the remote endpoint is kept as a fallback connection URL. |
Because every add surface (MCP, REST, CLI) funnels through the same keystone, a packages-only server is added as stdio and a remotes-only server as http identically across all surfaces.
See registry-add.md. New servers are quarantined by default until you approve them.