Skip to content

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.

PUT /v0.1/servers/{name}/versions/{version} with a bearer token. The server name is URL-encoded (io.github.acme/widgetio.github.acme%2Fwidget).

Terminal window
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"}]
}'

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 runs on every publish; you can also dry-run it without auth via POST /v0/validate, which returns an issues array instead of failing:

  • Namenamespace/server-name, namespace either io.github.<user> or reverse-DNS (reserved labels localhost, local, test, example, invalid rejected).
  • Version — semver.
  • Description — required, ≤ 100 characters.
  • Remotesstreamable-http or sse, with a URL. Draft records may use template-variable URLs (https://{host}/mcp).
  • PackagesregistryType (npm/pypi/oci/nuget/mcpb/cargo) + identifier.

Versions and servers move through activedeprecateddeleted:

Terminal window
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.

Verification proves you control the namespace you publish under, and flips namespaceVerified in the served _meta.

  1. StartPOST /namespace/verify with {"namespace": "..."}.
    • io.github.* → returns a GitHub OAuth auth_url; completing the flow proves you are the user or an org member. Requires GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET on the registry.
    • Domain namespaces → returns a challenge. Default is DNS; pass "method": "http" for the well-known-file variant.
  2. Fulfil the challenge
    • DNS: create a TXT record _mcp-registry-verification.<domain> with value mcp-registry-verify=<token>.
    • HTTP: serve the token at https://<domain>/.well-known/mcp-registry-verification.
  3. CheckPOST /namespace/verify/check with the namespace; the registry looks up the record (DNS-over-HTTPS) or fetches the file and marks the namespace verified.

Challenges expire after 24 hours; OAuth sessions after 10 minutes.

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