Contents5 sections
MCP4 min read
Publishing to the MCP registry, and the four things that stopped us
The official MCP registry is the discovery layer Anthropic shipped for Model Context Protocol servers. Getting listed took us four attempts. Every rejection was correct and none of them was obvious.
Mikhail Savchenko
The short answer
You publish with the mcp-publisher CLI against a server.json manifest. Authentication proves you control the domain your namespace names: you generate an Ed25519 keypair and serve the public half from /.well-known/mcp-registry-auth on that domain, then sign the publish with the private half. The things that rejected us were a description over 100 characters, a missing mcpName field in package.json, a namespace that did not match the domain we were proving, and a repository URL that pointed nowhere public.
Anthropic shipped an official registry for Model Context Protocol servers. An agent could already connect to one; until the registry there was no standard way for it to learn that a particular server existed.
We published the INITE Club server to it. The publishing flow is short and well designed. It still took four attempts, and each rejection taught us something that was not written down anywhere we had looked.
What a listing actually is
A registry entry is a server.json manifest validated against a published schema. Abridged, ours declares a name, a description, and the two ways to reach the same server — the real file also carries $schema, a top-level version and a version on the package entry, all of which are required:
{
"name": "club.inite/inite-club",
"description": "Ask the agent of someone whose calendar you could not get. No credential needed to try.",
"remotes": [
{ "type": "streamable-http", "url": "https://inite.club/api/mcp" }
],
"packages": [
{ "registryType": "npm", "identifier": "@inite/club-mcp",
"transport": { "type": "stdio" } }
]
}
Both entries point at the same club. The remote is the server; the package is an adapter for clients that speak stdio and nothing else, which is still most of them.
Proving you own the name
club.inite/inite-club is a reverse-DNS namespace, and it is a claim: this server belongs to whoever runs inite.club. The registry makes you demonstrate it.
You generate an Ed25519 keypair and serve the public half from the domain itself:
GET https://inite.club/.well-known/mcp-registry-auth
v=MCPv1; k=ed25519; p=<base64 public key>
Then you sign the publish with the private half.
mcp-publisher login http --domain inite.club --private-key <hex>
mcp-publisher publish
An account password would prove you own an account. Serving that file proves you control the domain the namespace names, which is the thing actually being claimed. There is a DNS variant, mcp-publisher login dns, that proves the same thing with a TXT record; we took the HTTP one because the site was already ours to deploy to and a file is quicker to rotate than a DNS record.
Which matters, because we had to rotate it. We lost the private key between one publish and the next, and recovery meant generating a new pair and replacing the served key before anything could ship. Keep it outside the repository and somewhere durable — ours now lives in ~/.config/inite/ at mode 0600.
The four rejections
A description over 100 characters. The field is capped, and the cap is shorter than the sentence most people write first. What came back named the field — clear once read, invisible if you are scanning for a stack trace. Ours took several drafts before it was both short enough and true.
A missing mcpName in package.json. If your listing attaches an npm package, that package has to claim the association back. The registry will not take your word that @inite/club-mcp is the package for club.inite/inite-club; the package has to say so too:
{ "name": "@inite/club-mcp", "mcpName": "club.inite/inite-club" }
Without it, anyone could attach anyone's package to their listing.
A namespace that did not match the domain being proved. The namespace, the served key and the domain in the login command all have to name the same domain. We had them disagree, and the error that comes back reads like a credentials problem when it is a naming one.
A repository URL that pointed nowhere public. The manifest carries a repository block, and ours named a repository that could not be fetched. Publish the code first, or leave the block out until you have.
What we would do differently
Write the manifest first, before the code is finished. Every one of those four rejections was a constraint on a decision we had already made — a name, a package, a description — and each was cheaper to satisfy before it had been written into three other places.
And attach the package. A listing with only a remote is honest but narrow: it serves the clients that already speak streamable HTTP and leaves everyone else with a config file to write by hand. The npm entry is what turns discovery into an install.
Getting listed is not the same as being found
A registry entry made our server discoverable to clients that read the registry. It did not, on its own, put us in front of anyone browsing. The directories that people actually read — the awesome-lists, the marketplaces, the index sites — each maintain their own catalogue, and they do not all consume the official one.
The registry has the clearest rules of any of them, which is why it is worth doing first. It is not the last thing to do.
By the numbers
- A registry entry is a server.json manifest validated against a published JSON schema; the rejections we got named the offending field rather than failing opaquely.
- The description field is capped at 100 characters, which is shorter than most people's first attempt.
- An npm package attached to a listing must carry an mcpName field in its package.json matching the server name, or the registry will not accept the association.
- Namespace ownership is proved with an Ed25519 signature against a public key the domain itself serves at /.well-known/mcp-registry-auth, not with an account password.
- A listing can carry both a remote endpoint and a package: ours advertises streamable HTTP at the endpoint and an npm package for clients that only speak stdio.
Questions
- Do I need the registry if my server is already on GitHub?
- They answer different questions. A repository tells a person where the source is. The registry tells an agent, and the clients that shop on an agent's behalf, that a server with this name exists, what transport it speaks, and which package installs it. Several directories and client install flows read the registry directly, so a listing is how you get discovered by something that is not a human reading a README.
- What is the difference between the remote and the package entry?
- The remote is the server itself: our club speaks streamable HTTP at one URL, and a client that supports it can connect with nothing installed. The package is the adapter for clients that only speak stdio, which is still most of them. Listing both means a client picks whichever it can actually use rather than failing on the one it cannot.
- Why prove the namespace against the domain rather than with a login?
- Because the namespace is a claim about a domain. club.inite asserts that this server belongs to whoever runs inite.club, and the only party who can demonstrate that is whoever can publish a file on it. An account password would prove you own an account, which is a different thing. Keep the private key somewhere durable: losing it means serving a new public key before you can publish again, which we know because we did it.
- How do updates work?
- You publish the manifest again with a new version. The registry keeps the entry and the name, so clients that pinned the name keep resolving. The version in server.json and the version of the published package have to agree; a listing pointing at a package version npm does not have is a listing that installs nothing.