Publishing servers
Publishing puts a server.json record (spec 2025-12-11) into the registry
under a version. Records are immutable per version: republishing an
existing version is rejected with 409 — publish a new version instead.
Publish a version
Section titled “Publish a version”PUT /v0.1/servers/{name}/versions/{version} with a bearer token. The server
name is URL-encoded (io.github.acme/widget → io.github.acme%2Fwidget).
curl -X PUT \ "http://localhost:5001/v0.1/servers/io.github.acme%2Fwidget/versions/1.2.0" \ -H "Authorization: Bearer $REGISTRY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "io.github.acme/widget", "description": "Widget lookup and provisioning tools", "version": "1.2.0", "repository": {"url": "https://github.com/acme/widget-mcp", "source": "github"}, "remotes": [{"type": "streamable-http", "url": "https://widget.acme.com/mcp"}] }'import requests
resp = requests.put( "http://localhost:5001/v0.1/servers/io.github.acme%2Fwidget/versions/1.2.0", headers={"Authorization": f"Bearer {token}"}, json={ "name": "io.github.acme/widget", "description": "Widget lookup and provisioning tools", "version": "1.2.0", "remotes": [{"type": "streamable-http", "url": "https://widget.acme.com/mcp"}], },)resp.raise_for_status()print(resp.json()["_meta"]["io.modelcontextprotocol.registry/official"])Response — the stored record with the official _meta block:
{ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", "name": "io.github.acme/widget", "description": "Widget lookup and provisioning tools", "version": "1.2.0", "remotes": [{"type": "streamable-http", "url": "https://widget.acme.com/mcp"}], "_meta": { "io.modelcontextprotocol.registry/official": { "status": "active", "publishedAt": "2026-08-30T09:14:02Z", "isLatest": true }, "ai.brutor.registry": { "downloadCount": 0, "verified": false, "governanceStatus": "unknown" } }}Republishing 1.2.0 now returns:
{"detail": "Version 1.2.0 already exists and is immutable"}Validation
Section titled “Validation”Validation runs on every publish; you can also dry-run it without auth via
POST /v0/validate, which returns an issues array instead of failing:
- Name —
namespace/server-name, namespace eitherio.github.<user>or reverse-DNS (reserved labelslocalhost,local,test,example,invalidrejected). - Version — semver.
- Description — required, ≤ 100 characters.
- Remotes —
streamable-httporsse, with a URL. Draft records may use template-variable URLs (https://{host}/mcp). - Packages —
registryType(npm/pypi/oci/nuget/mcpb/cargo) +identifier.
Lifecycle
Section titled “Lifecycle”Versions and servers move through active → deprecated → deleted:
curl -X PATCH \ "http://localhost:5001/v0.1/servers/io.github.acme%2Fwidget/status" \ -H "Authorization: Bearer $REGISTRY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"status": "deprecated", "statusMessage": "Superseded by widget2 — migrate by Q4"}'statusMessage (≤ 500 chars) and the change timestamp are served back in the
official _meta block as statusMessage / statusChangedAt, so consumers see
why and when. Deleted servers disappear from default listings but remain
visible as tombstones to incremental consumers.
Namespace verification
Section titled “Namespace verification”Verification proves you control the namespace you publish under, and flips
namespaceVerified in the served _meta.
- Start —
POST /namespace/verifywith{"namespace": "..."}.io.github.*→ returns a GitHub OAuthauth_url; completing the flow proves you are the user or an org member. RequiresGITHUB_CLIENT_ID/GITHUB_CLIENT_SECRETon the registry.- Domain namespaces → returns a challenge. Default is DNS; pass
"method": "http"for the well-known-file variant.
- Fulfil the challenge —
- DNS: create a TXT record
_mcp-registry-verification.<domain>with valuemcp-registry-verify=<token>. - HTTP: serve the token at
https://<domain>/.well-known/mcp-registry-verification.
- DNS: create a TXT record
- Check —
POST /namespace/verify/checkwith the namespace; the registry looks up the record (DNS-over-HTTPS) or fetches the file and marks the namespaceverified.
Challenges expire after 24 hours; OAuth sessions after 10 minutes.
Authentication & scopes
Section titled “Authentication & scopes”Any platform JWT signed with the shared JWT_SECRET can publish (in dev mode —
no secret configured — any bearer token authenticates as demo_user).
Publishing into an io.github.<owner> namespace additionally requires the
token subject to match the owner, unless the token is a Console service token
(admin:*).
Tokens may carry OAuth-style scopes; the check is additive:
| Token | Write routes |
|---|---|
| No scope claim (all existing tokens) | allowed — unchanged behavior |
scope: "mcp-registry:read" |
403 Token lacks mcp-registry:write scope |
scope: "mcp-registry:read mcp-registry:write" |
allowed |
scopes: ["mcp-registry:admin"] |
allowed |

