How to Publish an MCP Server to the Official MCP Registry
Publish an MCP server to the MCP Registry: pick a namespace, add the ownership proof to your package, write server.json, run mcp-publisher, automate in CI.
TL;DR. To publish an MCP server to the MCP Registry you write a server.json next to your code, put an ownership proof in the package it points to (an mcpName field in package.json for npm, an mcp-name: line in the README for PyPI, NuGet and Cargo, a label on a container image), log in with mcp-publisher login github or with a DNS or HTTP challenge on your own domain, and run mcp-publisher publish. The Registry stores metadata only: your code stays on npm, PyPI, a container registry or your own HTTPS endpoint. The name you choose is tied to how you log in: io.github.<user>/... for GitHub, com.example/... for a domain. Directories read the Registry on a schedule, so a published server reaches them without a second submission; mcp.tc reads it every hour.
What the MCP Registry stores
The MCP Registry is the official metadata repository for publicly accessible MCP servers, and it hosts pointers and descriptions rather than code. Its about page puts it this way: package registries such as npm, PyPI and Docker Hub host the packages, and "The MCP Registry hosts metadata that points to those packages." A remote server is published the same way, with its public URL in place of a package.
Two statements on that page shape everything below. The Registry "is currently in preview" and "Breaking changes or data resets may occur before general availability." And it "does not support private servers": a server on an internal network or a private package registry belongs in a private registry of your own, not here. The repository README adds that the API entered a freeze at version 0.1 on 24 October 2025, with no breaking changes "for the next month or more" while development continued on v0, so integrators could implement support against it even while the service is in preview (modelcontextprotocol/registry).
The Registry also says who it is for. It is "intended to be consumed primarily by downstream aggregators, such as MCP server marketplaces", which it expects to pull new metadata "on a regular but infrequent basis (for example, once per hour)". Host applications are expected to read those downstream registries through the same OpenAPI interface. Security scanning is delegated to the package registries and to the aggregators; the Registry itself does namespace authentication and metadata hosting. That is why publishing is mostly a matter of proving you own a name and a package.
Choose your namespace before you write server.json
Your server's name is decided by how you log in, so settle the namespace first. The authentication page gives two options:
| Login method | Name format | Example |
|---|---|---|
| GitHub-based | io.github.username/* or io.github.orgname/* | io.github.alice/example-server |
| Domain-based (DNS or HTTP) | com.example.*/*, the reverse-DNS form of your domain | com.example/example-server |
GitHub login is the quick route. mcp-publisher login github starts a device flow: the terminal prints a code, you enter it at https://github.com/login/device, and the CLI reports "Successfully logged in". Names must then start with io.github. followed by your username or organization.
Domain login takes a key pair and one record. For DNS, generate an Ed25519 key with openssl genpkey -algorithm Ed25519 -out key.pem, derive the public key, and publish a TXT record on the domain in the form v=MCPv1; k=ed25519; p=<public key>. After it propagates, log in with:
mcp-publisher login dns --domain "example.com" --private-key "${PRIVATE_KEY}"The HTTP variant uses the same key material but serves the proof as a file at https://example.com/.well-known/mcp-registry-auth, and you log in with mcp-publisher login http --domain "example.com" --private-key "${PRIVATE_KEY}". The page also documents ECDSA P-384 keys and signing through Google Cloud KMS or Azure Key Vault if you would rather not hold a raw private key.
Pick the domain route when the server is a company product and you want the name to match the company's domain. Pick GitHub when it is a personal or open-source project. The name is part of the published record and of the ownership proof in your package, so changing it later means re-proving and re-publishing.
Put the ownership proof in the package
The Registry checks that the package you point at claims the same server name, and each package type has its own place for that claim (package types):
| Package type | Where the proof goes | Supported registries |
|---|---|---|
| npm | "mcpName": "io.github.username/example-server" in package.json | https://registry.npmjs.org only |
| PyPI | mcp-name: io.github.username/example-server in the README (an HTML comment is fine) | https://pypi.org only |
| NuGet | the same mcp-name: string in the package README | https://api.nuget.org/v3/index.json only |
| Cargo | mcp-name: as visible text in the README | https://crates.io only |
| OCI image | LABEL io.modelcontextprotocol.server.name="..." in the Dockerfile | Docker Hub, ghcr.io, *.pkg.dev, *.azurecr.io, mcr.microsoft.com |
| MCPB bundle | a fileSha256 in server.json; the URL must contain the string "mcp" | GitHub and GitLab releases |
Two details from that page are easy to miss. For Cargo, the hidden-comment form does not work because "crates.io strips HTML comments during markdown→HTML conversion", so the token has to be ordinary Markdown text, such as a bullet in a links section. For MCPB, the Registry "does not validate this hash; however, MCP clients do validate the hash before installation", so compute it from the exact release asset with openssl dgst -sha256 example-server.mcpb.
Publish the package before you publish the server. The Registry only hosts metadata, and the quickstart walks through npm publish --access public first for that reason. The Registry verifies that the package matches the metadata, and the GitHub Actions guide's fix for "Package validation failed" is to verify that the package successfully published to its package registry.
Write server.json
A server.json is a small JSON file: a schema pointer, a name, a description, a version, and at least one of packages or remotes. mcp-publisher init writes a template "with some information derived from your project"; edit it rather than starting from nothing. For an npm package that runs over stdio it looks like this:
{
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
"name": "io.github.my-username/example-server",
"description": "Look up example records and create new ones.",
"repository": {
"url": "https://github.com/my-username/example-server",
"source": "github"
},
"version": "1.0.1",
"packages": [
{
"registryType": "npm",
"identifier": "@my-username/example-server",
"version": "1.0.1",
"transport": { "type": "stdio" },
"environmentVariables": [
{
"name": "EXAMPLE_API_KEY",
"description": "API key for the example service",
"isRequired": true,
"isSecret": true
}
]
}
]
}The name here must equal the mcpName in package.json. The 2025-12-11 schema sets the limits: name matches ^[a-zA-Z0-9.-]+/[a-zA-Z0-9._-]+$ and is 3 to 200 characters, description and the optional title are 1 to 100 characters, version is at most 255 characters and "Version ranges are rejected". Each package needs registryType, identifier and transport; a repository needs url and source.
For a remote server, replace packages with remotes (remote servers):
{
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
"name": "com.example/example-server",
"title": "Example Server",
"description": "Query example data over Streamable HTTP.",
"version": "2.0.0",
"remotes": [
{
"type": "streamable-http",
"url": "https://mcp.example.com/mcp"
}
]
}The rules for remotes are short. "A remote server MUST be publicly accessible at its specified URL." The type is streamable-http or sse; the page says to publish an sse entry only to support existing clients, which matches the 2026-07-28 changelog, where the HTTP+SSE transport is reclassified as Deprecated. A URL may carry {curly_brace} variables with a variables map (description, isRequired, choices, default) for multi-tenant endpoints, and a headers array tells clients which headers to send, each marked isRequired or isSecret. remotes and packages can sit in the same file so a client can choose the hosted endpoint or the local install.
If you built the server with mcp.tc's CLI, npx mcp-tc card writes a server card and a server.json for the official Registry (mcp-tc). Check the generated name and version against the table above, then publish it with mcp-publisher as below.
Install mcp-publisher, log in and publish
Publishing is three commands once the file is ready. Install the CLI from the quickstart's release download or with Homebrew:
curl -L "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher && sudo mv mcp-publisher /usr/local/bin/
# or
brew install mcp-publishermcp-publisher --help lists four commands: init, login, logout and publish. Then, in the project directory:
mcp-publisher login github # or: login dns / login http, see above
mcp-publisher publishA successful run prints Publishing to https://registry.modelcontextprotocol.io... followed by the server name and version. Confirm with the read API, which is the same endpoint aggregators use:
curl "https://registry.modelcontextprotocol.io/v0.1/servers?search=io.github.my-username/example-server"The response has a servers array and a metadata object with count and nextCursor. Each entry carries your server.json plus a _meta key, io.modelcontextprotocol.registry/official, with status, publishedAt, updatedAt and isLatest. The API reference also documents GET /v0.1/servers/{serverName}/versions, /versions/latest, and the updated_since, version, cursor and limit parameters.
The quickstart names three errors and their fixes. "Registry validation failed for package" means the ownership proof is missing or does not match: check mcpName or the mcp-name: line against name. "Invalid or expired Registry JWT token" means log in again. "You do not have permission to publish this server" means the name does not fit the login method; with GitHub auth, it must start with io.github.<your-username>/.
Automate it from GitHub Actions
A tag push can publish both the package and the Registry entry, with no stored Registry secret if you use OIDC (GitHub Actions guide). The job needs id-token: write and contents: read, installs the CLI with the same curl line, and logs in with github-oidc:
name: Publish to MCP Registry
on:
push:
tags: ["v*"]
jobs:
publish:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: "lts/*"
- run: npm ci
- run: npm run build --if-present
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Install mcp-publisher
run: |
curl -L "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher
- run: ./mcp-publisher login github-oidc
- run: ./mcp-publisher publishThe guide's full example also runs tests and has an optional jq step that copies the tag into server.json (.version = $v), which keeps the file and the tag from drifting. For a domain namespace, swap the login step for ./mcp-publisher login dns --domain example.com --private-key ${{ secrets.MCP_PRIVATE_KEY }}; for a personal access token instead of OIDC, the guide uses login github --token with a MCP_GITHUB_TOKEN secret that has the read:org and read:user scopes. Push git tag v1.0.1 && git push origin v1.0.1 and the workflow does the rest.
After you publish: aggregators, directories and mcp.tc
A Registry entry is the upstream copy; what people see comes from the aggregators that read it. mcp.tc is one of them. Its checker reads the official Registry every hour, and a server published there under your own namespace gets a listing that carries that name (server owners docs). The checker also reads the server itself, identifying as mcp.tc-bot/1.0 (+https://mcp.tc/about) and stopping after 25 requests or 60 seconds: the handshake, the tool list with its hints, 401 responses and OAuth metadata for sign-in, the server card, and the repository and package pages. A suggestion goes live on its own only when the server answered, nothing was flagged, and the name or vendor matches the domain; otherwise a moderator looks. On 14 October 2026 the directory listed 844 servers.
If you do not want to wait for the hourly read, suggest the server by URL, or run npx mcp-tc submit https://mcp.example.com/mcp from a terminal, which sends the suggestion and waits for the result. npx mcp-tc check http://localhost:3000/mcp reads a server the way mcp.tc does and lists what to fix before you publish anywhere.
Two things are worth knowing about the downstream copy. First, mcp.tc's own API at https://mcp.tc/v0.1/servers is a read-only copy of the official Registry's read API (version 0.1, server.json schema 2025-12-11), filled with mcp.tc's approved listings, and its entries never carry the io.modelcontextprotocol.registry/official metadata because mcp.tc is not the official Registry; it adds a tc.mcp/listing key instead. If your tooling checks that official key to confirm an entry came from the Registry, it will behave correctly against both. Second, the Registry's DNS proof and mcp.tc's verified checkmark are separate records. The Registry's is v=MCPv1; k=ed25519; p=...; mcp.tc's is an mcp-tc-verification= TXT record that it checks every day, and the checkmark says who runs a server, not whether it is safe.
Once listed, a new version you publish to npm or PyPI, or that a remote server reports, is recorded on the listing's History tab (at most one version change in 24 hours), and anyone who follows the listing can get an email when the number goes up (how the history works). That is a reason to keep version in server.json, the package version and the serverInfo.version the server reports in step: readers who pinned your package will compare them.
FAQ
Do I need to publish to the MCP Registry to be listed on mcp.tc?
No. mcp.tc builds listings from public data, and the official Registry is one source it reads every hour, alongside a live MCP handshake and tools list, server cards, GitHub, npm and PyPI. You can also suggest a server by URL on the Submit page, or with npx mcp-tc submit. A Registry entry does help: a listing created from it carries the name you published under, so your server appears with the identity you chose.
Can I publish a remote server without any package?
Yes. Put a remotes array in server.json with "type": "streamable-http" and the public URL, and leave out packages. The Registry requires that the server be publicly reachable at that URL; a server on a private network does not qualify. You still need a namespace, so log in with GitHub for an io.github. name or with a DNS or HTTP challenge for a name under your own domain.
What is the difference between GitHub login and DNS login?
They decide your namespace. GitHub login (mcp-publisher login github, a device-code flow) lets you publish under io.github.<username>/ or io.github.<org>/. DNS login proves control of a domain with a TXT record of the form v=MCPv1; k=ed25519; p=<public key> and lets you publish under the reverse-DNS form, such as com.example/. HTTP login does the same with a file at /.well-known/mcp-registry-auth. CI can use GitHub OIDC with no stored secret.
Why does mcp-publisher say "Registry validation failed for package"?
The Registry fetched your package from its registry and did not find a matching ownership claim. For npm, add "mcpName" to package.json with exactly the name from server.json. For PyPI and NuGet, add mcp-name: <server name> to the README, which an HTML comment can hide. For Cargo, the token must be visible text. For a container image, add the io.modelcontextprotocol.server.name label. Then publish the package again before you retry.
Is the MCP Registry stable enough to depend on?
The Registry's own pages say it is in preview and that breaking changes or data resets may occur before general availability, so keep server.json and your publishing workflow in your repository and be ready to republish. The repository README states that on 24 October 2025 the API entered a freeze at version 0.1, with no breaking changes "for the next month or more" while development continued on v0. If you consume the API, code against /v0.1/servers and the official _meta key.
Does publishing to the Registry mean my server has been reviewed?
No. The Registry checks that you control the namespace and that the package claims the same name; it delegates security scanning to the package registries and to downstream aggregators. The published entry proves identity, not safety. Likewise, mcp.tc's verified checkmark says who runs a server, and its History tab only shows what its reads of a tool list saw at each check. Neither is a security review of the code.
Sources
- modelcontextprotocol.io/registry/about
- modelcontextprotocol.io/registry/quickstart
- modelcontextprotocol.io/registry/authentication
- modelcontextprotocol.io/registry/package-types
- modelcontextprotocol.io/registry/remote-servers
- modelcontextprotocol.io/registry/github-actions
- github.com/modelcontextprotocol/registry
- registry.modelcontextprotocol.io/docs
- static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json
- modelcontextprotocol.io/specification/2026-07-28/changelog