Publish a Remote MCP Server to the Official Registry
The complete 2026 walkthrough — no npm package, no npx, no install step for your users.
Your hosted URL is the package. Written from a server that is actually listed.
npx -y your-server. That is the hard path: it needs an npm account, it ships your code to the client, and you
can never meter or update it. The official registry also accepts remote servers — you point it at a URL you
already host. This guide is that path, end to end.
1. Remote vs. local: pick remote
Local (npm / npx) | Remote (hosted URL) | |
|---|---|---|
| Accounts needed | npm account (often CAPTCHA-walled) | None beyond a GitHub token |
| User install step | npx -y your-server | Paste a URL |
| Ship updates | Publish a version, users must upgrade | Redeploy, everyone gets it |
| Secrets / API keys | Live on the user's machine | Stay on your server |
| Usage metering & billing | Effectively impossible | Server-side, trivial |
| Cold start | Package download per run | Already warm |
If you ever want to charge for your tool, remote is the only shape that works — the paywall has to live somewhere you control. The same is true for rate limits and abuse handling.
2. What a remote MCP server actually has to answer
Streamable HTTP is one POST endpoint speaking JSON-RPC 2.0. You need to handle four things. That is the
whole protocol surface for a tools-only server:
initialize— reply with yourprotocolVersion,capabilities.tools, andserverInfo.notifications/*— any method starting withnotifications/has noid. Reply HTTP 202 with an empty body. Returning a JSON-RPC result here breaks strict clients.tools/list— an array of{name, description, inputSchema}. TheinputSchemais JSON Schema; clients show it to the model, so write the descriptions for a reader who cannot see your docs.tools/call— run it, return{content:[{type:"text",text:"..."}]}. For a failure the model should see, returnisError:trueinside a normal result rather than a JSON-RPC error — the model can then read the message and recover.
ping with an empty result object; some clients health-check with it before ever calling tools/list.3. Publishing to the official registry
The registry is registry.modelcontextprotocol.io. Authentication is the part everyone gets stuck on: you do
not need a registry account. You exchange a GitHub token for a short-lived registry JWT, which grants you the
namespace io.github.<your-github-username>/*.
Step 1 — trade your GitHub token for a registry JWT
curl -s -X POST https://registry.modelcontextprotocol.io/v0/auth/github-at \
-H 'Content-Type: application/json' \
-d '{"github_token":"ghp_your_token_here"}'
# -> {"token":"eyJ...","expires_at":...}
A classic PAT or a fine-grained token both work. The registry only verifies who owns the token; it never fetches your public profile, so this works even for accounts with no public repos at all.
Step 2 — write server.json
{
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json",
"name": "io.github.yourname/yourserver",
"description": "One line, 100 characters maximum.",
"version": "1.0.0",
"repository": { "url": "https://github.com/yourname/yourrepo", "source": "github" },
"remotes": [
{ "type": "streamable-http", "url": "https://your.domain/yourserver/mcp" }
]
}
Step 3 — validate free, then publish
# dry run, costs nothing and never publishes
curl -s -X POST https://registry.modelcontextprotocol.io/v0/validate \
-H 'Content-Type: application/json' -d @server.json
curl -s -X POST https://registry.modelcontextprotocol.io/v0/publish \
-H "Authorization: Bearer $REGISTRY_JWT" \
-H 'Content-Type: application/json' -d @server.json
- The body is the server object at the TOP LEVEL. Wrapping it as
{"server": {...}}returns a confusing 422 that does not tell you this. This is the single most common failure. descriptionmust be ≤ 100 characters. Longer descriptions 422 as a validation error that reads like a schema problem.- The JWT expires in about five minutes. Mint it in the same script that publishes; do not paste it in by hand.
- Your namespace is fixed to your GitHub username.
namemust startio.github.<username>/exactly, or you get a permission error rather than a naming error.
Step 4 — confirm it is really live (unauthenticated)
curl -s 'https://registry.modelcontextprotocol.io/v0/servers/io.github.yourname%2Fyourserver/versions'
# want: "status":"active" and "isLatest":true
Always check this with no credentials attached. Authenticated requests can show you a version of the
world only you can see — an unauthenticated curl is the only honest test that a stranger can find your server.
4. What listing actually gets you
The official registry is the upstream source for the places people and agents actually browse — mcp.so, PulseMCP, Glama, and the server pickers built into VS Code and Cursor. One publish propagates to all of them, which is the real reason to do this rather than only posting a link somewhere.
Two things worth doing the same day: serve a /llms.txt at your origin describing your tools in plain text,
and keep your root path returning a real HTML page. Both are cheap and both are crawled.
5. Hosting notes that save a redeploy
- Zero dependencies deploy fastest. Node's built-in
httpmodule is genuinely enough for a streamable-HTTP MCP server. Droppingpackage.jsonentirely skips the install step at boot. - Never read a static asset at module scope. One
readFileSyncoutside a handler will crash the process before it listens if the file is ever missing, and you get no logs explaining why. Wrap it in try/catch with an inline fallback. - If you are mounted under a path prefix (
example.com/yourserver), make the prefix host-aware and advertise the fully-qualified URL. A hard-coded prefix corrupts every URL you publish on any other host. - Bind
0.0.0.0, notlocalhost, or your platform's health check never passes.
6. The starter kit
Everything above is free and complete — follow it and you will get listed. The kit is for skipping the two hours of assembly and the gotchas above:
server.js— a complete zero-dependency remote MCP server:initialize,ping,tools/list,tools/call, correct 202 notification handling, batch rejection, and a bounded body reader. Two working example tools to clone. Runs on plain Node, no build step.publish.sh— mints the JWT and publishes in one command, so the five-minute expiry can never bite you.server.jsontemplate, pre-validated against the current schema.- A metering hook — the free-tier / paid-key branch point already wired into
tools/call, so you can charge later without restructuring. - Stateless API keys — an HMAC key scheme that needs no database and survives redeploys on ephemeral-filesystem hosts, where a key table would be wiped.
llms.txtandopenapi.jsontemplates for the discovery surfaces in §4.
MIT licensed — use it in commercial work, no attribution required.
How delivery works: the kit is sent to the email address on your Stripe receipt, by hand, normally within a few hours and always within 24. This is a one-person shop and that is the honest timing — if you need it this minute, §2, §3 and §5 above contain every step and every gotcha, and cost nothing. Any problem, reply to your Stripe receipt and you get a refund, no questions.
A server built exactly this way
Peek API is listed in the official registry as
io.github.smeltworks-labs/peek, serving peek_preview and peek_markdown over remote MCP at
https://smeltworks.com/peek/mcp. Every claim on this page came from getting that one live — including all four
publish gotchas, each of which cost a real debugging session.