MCP Registry
Otoroshi can publish your governed MCP virtual servers to the standard MCP registry format, so that registry-aware MCP clients in your organization discover the sanctioned servers automatically — and connect through the gateway (so OAuth and the Zero-Trust controls apply end to end).
It is the difference between "a pile of connection configs" and "a curated, approved, browsable, versioned catalogue of sanctioned MCP capabilities".
How it works
A registry is essentially a list of server.json documents, one per MCP server. In Otoroshi a registry-server maps 1:1 to a virtual server: the server.json is a projection of the virtual server. There is no separate entity — publication is a small block of metadata on the virtual server itself.
Publishing is a single switch on the virtual server, governed by Otoroshi's roles/RBAC: a server appears in the catalogue when you flip published, and you can mark it deprecated without removing it. That switch is your approval gate — nothing reaches consumers until you say so.
Run several registries, each a curated slice
A registry is just a route with the API plugin, so you can stand up as many as you like — each exposing a different, curated subset of your published servers. The Expose setting on the registry API (and the preset) decides what a given registry serves:
| Mode | Serves |
|---|---|
| All published servers (default) | every published server |
| Selected servers (by ref) | only the virtual servers you pick |
| By tag | every published server carrying any of the chosen tags |
| Route's tenant | every published server in the route's tenant |
So one route can be your org-wide catalogue, another a prod-only registry by tag, and a per-team registry falls out naturally from multi-tenancy — the same servers, behind different governed front doors. Selection always composes with publication: a server still has to be published to appear anywhere.
The registry block
Configured on the MCP Virtual Server entity (entity-level metadata, not a per-route override):
| Field | Type | Default | Description |
|---|---|---|---|
published | boolean | false | List this server in the registry endpoints. This is the approval gate. |
name | string | — | Reverse-DNS namespaced name (e.g. io.acme/github). Defaults to a slug of the server name (io.cloud-apim/<slug>). |
version | string | 1.0.0 | Semantic version advertised in server.json. |
title | string | — | Human-readable title. Defaults to the server name. |
url | string | — | Public streamable-http URL exposed for this server (the server.json remote). The editor has a "Detect from route" assistant that scans the route(s) exposing this server, suggests the URL and shows the detected auth (OAuth / apikey / mTLS) — you confirm or override. If empty, no remote is published (clients still discover auth via the server's own .well-known). |
deprecated | boolean | false | Marks the registry status as deprecated while keeping the entry listed. |
The server.json projection
A published server is projected to the official server.json format (schema 2025-12-11). The server.json itself has no status field — the registry layer adds it under the _meta envelope:
{
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
"name": "io.acme/github",
"description": "Hardened GitHub MCP via Otoroshi",
"title": "GitHub",
"version": "1.0.0",
"remotes": [{ "type": "streamable-http", "url": "https://gw.acme/mcp/github" }],
"_meta": {
"io.modelcontextprotocol.registry/official": { "status": "active", "isLatest": true },
"com.cloud-apim.otoroshi/governance": { "id": "mcp-virtual-server_xxx", "tags": ["platform"] }
}
}
name/version/titlecome from theregistryblock (with the slug / entity-name fallbacks).remotesis emitted only whenurlis set; the type is alwaysstreamable-http.statusisactive, ordeprecatedwhenregistry.deprecatedis on.- Governance metadata (entity id, tags) is added under our own reverse-DNS
_metakey, which does not break the standard.
One-click setup — the registry preset
The simplest way to expose the registry is the Cloud APIM - MCP Registry preset. Add it to a route and it wires both plugins at once:
| Option | Default | Wires |
|---|---|---|
well_known_path | /.well-known/mcp-registry | the discovery document |
api_base_path | /v0 | the registry API on /v0/.* |
registry_url | — | base URL advertised by the discovery doc (defaults to this host + base path) |
schema_version | 2025-12-11 | the server.json schema version |
So out of the box the discovery doc is served on /.well-known/mcp-registry and the API on /v0/servers. If you'd rather wire the plugins by hand, the two are described below.
Exposing the registry — two plugins
Two plugins serve the registry — mount them on a route on your gateway. The catalogue is governed end to end: only enabled, published servers are ever served, so your approval gate decides exactly what consumers can discover.
Cloud APIM - MCP Registry API
Serves the standard registry REST API. Put it on a route matching your registry base path (e.g. /v0/servers):
GET /v0/servers— the listing, cursor-paginated:Pages hold up to 200 servers by default — plenty for a curated catalogue, so most deployments get everything in one call. Pass{
"servers": [ { "name": "io.acme/github", "version": "1.0.0", "...": "..." } ],
"metadata": { "count": 1 }
}?limit=to change the page size. When more servers remain, the response carries ametadata.next_cursor; fetch the next page with?cursor=<value>. It's standard cursor pagination, so registry-aware clients walk the whole catalogue automatically.GET /v0/servers/{name}— a single server by its (possibly namespaced) name, e.g.GET /v0/servers/io.acme/github. Returns theserver.json, or404if it is unknown or not published.
Cloud APIM - MCP Registry discovery (.well-known)
Serves a small discovery document pointing clients at the registry API. Put it on your well-known path.
{
"registry": "https://gw.acme/v0",
"servers_endpoint": "https://gw.acme/v0/servers",
"schema_version": "2025-12-11",
"server_json_schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json"
}
The discovery path is configurable on the route, so you can serve it wherever your clients expect to find it.
End-to-end flow
An admin approves GitHub (a virtual server) by setting
registry.published = true→ the registry lists it as aserver.json(io.acme/github, remotehttps://gw.acme/mcp/github) → any registry-aware MCP client in the company discovers it → connects through the gateway → governed + Zero-Trust-protected end to end.
The discovery endpoint funnels clients onto the governed path: instead of each team hand-configuring connections, a client just needs the registry URL and auto-discovers every sanctioned server (with its auth requirements).