How to publish an MCP server to the official MCP Registry
Publishing to the official MCP Registry means submitting metadata, not code. You publish your
package to npm, PyPI, or a container registry first; then you verify ownership of a namespace, describe
the server in a server.json file, and submit an immutable version with the mcp-publisher CLI.
The registry is currently in preview: breaking changes or data resets may occur before general availability. Publish to it, but do not treat it as the only place your server is discoverable.
What the registry is, and is not
The MCP Registry is a centralised metadata repository for publicly accessible MCP servers, backed by contributors including Anthropic, GitHub, PulseMCP, and Microsoft. It provides a single publishing point, namespace management through DNS and GitHub verification, a REST API for clients and aggregators, and standardised installation information.
Three boundaries are worth understanding before you start.
It stores metadata, not artifacts. Package registries — npm, PyPI, Docker Hub — host the code. The
registry hosts the record that maps "weather v1.2.0" to npm:weather-mcp. Your package has to exist
before its metadata can.
It does not scan your code. Security scanning is delegated to the underlying package registries and to downstream aggregators. A registry entry is evidence of namespace ownership, not of safety. That is worth saying out loud to your own users, and it is why installing anything still calls for vetting the server yourself.
It does not host private servers. A server qualifies if its installation method is publicly available or the server itself is publicly reachable. Servers on private networks or private package registries do not belong there; host your own registry instead.
It is also not meant to be consumed directly by host applications. The expected pattern is that downstream aggregators and marketplaces pull from the registry API on a regular but infrequent basis and add curation, ratings, and richer metadata on top.
Before you publish
You need four things in place:
- A working server. Tested against its own tool list, not just launched once. If you are still building, start with how to build an MCP server in TypeScript.
- A published package, or a publicly reachable URL for a remote server.
- A namespace you can prove you own — a GitHub account or a domain.
- A version string you will never reuse. Published versions are immutable.
Step 1: Add verification metadata to the package
The registry verifies that a server's underlying package matches its metadata. For npm packages, that
means adding an mcpName property to package.json:
{
"name": "@my-username/mcp-weather-server",
"version": "1.0.1",
"mcpName": "io.github.my-username/weather",
"repository": {
"type": "git",
"url": "https://github.com/my-username/mcp-weather-server.git"
}
}
The value of mcpName becomes your server's name in the registry, and it must match the namespace you
authenticate with. With GitHub authentication it must start with io.github.<your-username>/.
Other package types have their own equivalent verification step; check the package-types guide for the registry you are publishing to.
Step 2: Publish the package first
Because the registry only stores metadata, the artifact has to exist before the record can point at it.
npm install
npm run build
npm adduser # if not already authenticated
npm publish --access public
Confirm the package is live at its public URL before continuing. A registry submission that points at a package that does not exist yet fails validation rather than waiting for it.
Step 3: Install mcp-publisher
# macOS / Linux, via Homebrew
brew install mcp-publisher
# Or a pre-built binary
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/
mcp-publisher --help should list four commands: init, login, logout, and publish.
Step 4: Create server.json
mcp-publisher init generates a template with fields derived from your project.
{
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
"name": "io.github.my-username/weather",
"description": "An MCP server for weather information.",
"repository": {
"url": "https://github.com/my-username/mcp-weather-server",
"source": "github"
},
"version": "1.0.1",
"packages": [
{
"registryType": "npm",
"identifier": "@my-username/mcp-weather-server",
"version": "1.0.1",
"transport": { "type": "stdio" }
}
]
}
The name in server.json must match the mcpName in package.json. That pair is the link the
registry checks.
Delete template fields you do not need. The generated file includes an environmentVariables example;
leaving a placeholder YOUR_API_KEY entry in a server that needs no credential is a small thing that
makes your listing look careless and makes installation confusing.
Where a server does need configuration, describe it properly — each variable takes a description,
isRequired, format, and isSecret. Marking a credential isSecret is how a host knows not to
display or log it.
Step 5: Authenticate the namespace
mcp-publisher login github
This starts a device flow: visit the printed URL, enter the code, authorise, and return to the terminal.
DNS authentication is the alternative and the one to use for a custom domain prefix such as
com.example/analytics. It proves control of the domain rather than a GitHub account, and it is what
makes an organisation's servers recognisably theirs.
This namespace system is the registry's actual trust mechanism: reverse-DNS names tie a server to a verified GitHub account or domain, so only the legitimate owner can publish under that prefix.
Step 6: Publish
mcp-publisher publish
Verify through the API rather than trusting the success message:
curl "https://registry.modelcontextprotocol.io/v0.1/servers?search=io.github.my-username/weather"
Versioning rules that will bite you
Every publication needs a unique version string, and once published, the version and its metadata cannot be changed. There is no edit. There is only a new version.
Semantic versioning is recommended but not required. The registry parses versions as semver for sorting and marks the newest appropriately; if parsing fails, that version is always marked "latest."
| Example | Type | Guidance |
|---|---|---|
1.0.0 |
semantic version | Recommended |
2.1.3-alpha, 1.0.0-beta.1 |
semantic prerelease | Recommended |
2025.11.25 |
semantic date | Recommended |
2025.06.18 |
non-semantic date | Allowed, with caution |
v1.0 |
prefixed | Allowed |
^1.2.3, 1.x, >=1.2.3 |
version range | Prohibited |
Three traps follow from those rules.
Mixing schemes promotes the wrong release. If you use semver and then publish something that is not valid semver, the new version is marked "latest" even when it would sort earlier.
Prerelease versions sort before their release. Publishing 1.2.3-1 after 1.2.3 means the
prerelease is not marked latest — which is either what you wanted or a silent no-op.
Metadata-only updates need a version anyway. If you expect to republish without changing the
package — to fix a description, add an environment variable, correct a URL — use prerelease versions
such as 1.2.3-1 for those, and keep the package version field pointing at the real package release.
Align versions deliberately: for local servers, match the server version to the package version; for remote servers with an API version, match the server version to the API version; with multiple packages, use the server version as the overall release number.
Publishing a remote server
A remote server is published through remotes instead of packages, and must be publicly accessible
at its URL:
{
"name": "com.example/acme-analytics",
"version": "2.0.0",
"remotes": [
{ "type": "streamable-http", "url": "https://analytics.example.com/mcp" }
]
}
Use streamable-http. The SSE transport is deprecated; publish an "sse" remote only to support
existing clients, and plan its removal. A server can expose both at different URLs during a migration.
Two remote-only features are worth knowing:
URL template variables support multi-tenant deployments. A {tenant_id} or {region} placeholder
with a variables block — including description, isRequired, choices, and default — lets one
entry describe many endpoints, and the host prompts the user to fill it in.
Declared headers tell clients which HTTP headers to send, with isRequired and isSecret flags.
Use this for API-key authentication; for OAuth, see
MCP authentication explained.
remotes and packages can coexist in one entry, letting the host application offer the user a
choice between a hosted endpoint and a local install.
Troubleshooting
| Error | What to do |
|---|---|
| "Registry validation failed for package" | The package is missing its verification metadata — check mcpName in package.json and that it matches name in server.json. |
| "Invalid or expired Registry JWT token" | Re-run mcp-publisher login github. |
| "You do not have permission to publish this server" | Your namespace does not match your authentication method. With GitHub auth, the name must start with io.github.<your-username>/. |
After publishing
Automate it. Publishing by hand from a laptop means the registry drifts from your releases, and drift
in an immutable record is not recoverable — only appendable. Wire mcp-publisher into the same CI job
that publishes the package, so the two always move together.
Then treat the registry as one distribution channel rather than the destination. It is deliberately unopinionated: no ratings, no curation, no review. The richer discovery layer is downstream, in marketplaces and catalogues that add the context the registry leaves out.
Next step: Publish verified MCP metadata, then submit the reviewed server to LLM Mart for richer discovery and trust signals — the registry proves who you are, the catalogue explains what the server is for.
Sources
Comments (0)
Sign in to join the conversation.
No comments yet.