All posts
technicaltutorial

Publishing an MCP server to the official registry: mcpName, domain proof and the npm traps

PreviewDrop Team·September 16, 2026·8 min read

PreviewDrop's MCP server is now listed in the official MCP registry as dev.previewdrop/cli, backed by the npm package previewdrop. The registry part took an afternoon. Most of that afternoon went on things the tooling does not tell you until the last step, so this is the order of operations we wish we had started with, and the errors we hit on the way.

It started with a bug of ours, which is worth telling first because the same mistake is easy to make in any stdio MCP server.

The bug: exiting before the transport connects

npx previewdrop mcp starts a stdio MCP server. Until this week, if no API key was stored, it wrote one line to stderr and called process.exit(1) before connecting the transport. Every editor that launched it reported the same thing: the server failed to start. The one line that explained why went to a stderr stream most people never open.

Our VS Code extension launches the server for you, so anyone who installed the extension before running previewdrop login in a terminal hit exactly this.

The fix is a rule we now apply to any MCP server: never exit before the transport connects. Start, connect, list your tools, and turn missing configuration into a tool error:

function notConnected() {
  return {
    isError: true,
    content: [{
      type: 'text',
      text: 'PreviewDrop is not connected yet. Run this once in a terminal:\n\n' +
            '    npx previewdrop login\n\n' +
            'Restart this MCP server afterwards and every tool will work.',
    }],
  };
}

// In the CallTool handler: if there is no key, return notConnected()
// instead of calling the API. The ListTools handler never needs the key.
await server.connect(new StdioServerTransport());

Now the server starts without a key, advertises all 14 tools so the agent can see what exists, and every call answers with the one command that fixes it. The agent relays that to the user, which is a far better first experience than a red "failed" badge.

We fixed that, released it as previewdrop@0.1.12, and then listed it.

Step 1: write server.json

The registry does not host your code. It stores a manifest that points at a package somewhere else, npm in our case. Ours, trimmed of the optional environment variable block:

{
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
  "name": "dev.previewdrop/cli",
  "title": "PreviewDrop",
  "description": "Deploy any Git branch to a live HTTPS preview URL from your agent. No Dockerfile required.",
  "version": "0.1.12",
  "websiteUrl": "https://previewdrop.dev",
  "packages": [
    {
      "registryType": "npm",
      "identifier": "previewdrop",
      "version": "0.1.12",
      "runtimeHint": "npx",
      "transport": { "type": "stdio" },
      "packageArguments": [
        { "type": "positional", "value": "mcp", "valueHint": "subcommand", "isRequired": true }
      ]
    }
  ]
}

Two things to know before you run anything.

description is capped at 100 characters. Go over it and the publish is rejected with HTTP 422. Ours is 90. Write it as a sentence an agent can act on, not a tagline.

packageArguments matters when the server is a subcommand. Our binary is a general CLI; the MCP server is previewdrop mcp. Without the positional argument, a client following the manifest would start the CLI, not the server.

Step 2: add mcpName to package.json and publish to npm first

This is the trap that costs a release if you hit it in the wrong order.

The registry verifies that the npm package belongs to the server you are listing by reading mcpName from the package's published package.json. It must equal the name in server.json exactly:

{
  "name": "previewdrop",
  "version": "0.1.12",
  "mcpName": "dev.previewdrop/cli"
}

mcp-publisher validate checks your server.json against the schema. It does not check that mcpName exists in the package, and it cannot check what npm is actually serving. So a manifest can validate cleanly and still be refused at publish time.

The order that works:

  1. Add mcpName to package.json.
  2. npm publish a version that contains it.
  3. Wait until npm is actually serving that version.
  4. Point server.json at that exact version and publish to the registry.

Never list a version in the registry while npm still serves an older one. The registry entry is what clients install from.

npm traps along the way

An unauthorised token says 404, not 403

If the token npm is using cannot publish the package, you may not get a permission error. We got this:

npm error code E404
npm error 404  'previewdrop@0.1.12' is not in this registry.

That reads like a typo in the package name. It was an authentication problem. Run this first and read the username it prints:

npm whoami

2FA and --otp

npm publish --otp=123456 only works if the account has two-factor authentication turned on. If it does not, there is no code to pass. What worked for us was a granular access token with the "bypass 2FA" option. npm is phasing that option out; trusted publishing from a CI provider is its replacement, and it is the better long-term setup if you publish from CI.

npm publish is asynchronous

npm publish printed + previewdrop@0.1.12 and exited 0. Immediately afterwards:

npm view previewdrop@0.1.12 version

returned a 404. For us it took about 60 seconds before the version resolved. The publish had worked; the read side had not caught up. Poll, do not republish. A second publish of the same version is refused anyway, and a version bump just to get past a delay leaves you with a release that exists for no reason.

until npm view previewdrop@0.1.12 mcpName; do sleep 10; done

Waiting on mcpName rather than version checks the thing the registry is about to check.

npx can run a stale copy

After publishing, npx -y previewdrop@0.1.12 mcp is the obvious smoke test. It can also run a copy from npx's cache rather than what npm is serving now. To test the published artifact, install it somewhere clean and run the binary directly:

mkdir /tmp/pd-check && cd /tmp/pd-check
npm init -y >/dev/null
npm install previewdrop@0.1.12
./node_modules/.bin/previewdrop --version

While we were in the tarball, npm publish --dry-run showed a second problem: our compiled test files were shipping to every install. A separate tsconfig.build.json that excludes tests fixed it. Note that tsc does not delete stale output, so clean dist before building or the old files stay in the tarball.

Step 3: prove you own the domain

Names under io.github.<user>/ authenticate with GitHub. A name like dev.previewdrop/cli is a reversed domain, and the registry wants proof you control previewdrop.dev. We used the HTTP method.

Generate an Ed25519 key pair and keep the private half out of your repository:

openssl genpkey -algorithm Ed25519 -out key.pem
chmod 600 key.pem

Serve the public half at https://<your-domain>/.well-known/mcp-registry-auth, as a single line in this form:

v=MCPv1; k=ed25519; p=<base64 public key>

For us that is a static file in the site's public/.well-known/ directory, so it deploys with the site. Only the public key is in it, so it is safe to commit. Check it before logging in:

curl -s https://previewdrop.dev/.well-known/mcp-registry-auth

Step 4: log in and publish as one step

mcp-publisher wants the private key as hex, not PEM. This pulls the 64 hex characters out of the key file without writing them anywhere:

PRIVATE_KEY_HEX=$(openssl pkey -in key.pem -noout -text | grep -A3 "priv:" | tail -n +2 | tr -d ' :\n')
mcp-publisher login http --domain previewdrop.dev --private-key "$PRIVATE_KEY_HEX"
mcp-publisher publish

Three things bit us here:

  • The login token expires quickly. Log in, then publish immediately, in the same script. If you log in, go and fix a description, and come back, log in again.
  • mcp-publisher keeps one session for all domains. Logging in for a second domain replaces the first. If you publish servers under several namespaces, log in right before each publish.
  • The proof is fetched live. If the site serving /.well-known/mcp-registry-auth is down or mid-deploy, login fails. Do not publish to the registry while the site is redeploying.

Step 5: check the listing

The registry has a public read API, so you can confirm the entry and the version it points at:

curl -s "https://registry.modelcontextprotocol.io/v0/servers?search=dev.previewdrop"

Check version and packages[0].version in the response. Both should match what npm view returns.

The order, in one place

  1. Fix the server so it starts and lists tools without configuration.
  2. Add mcpName to package.json, equal to the server.json name.
  3. npm whoami, then npm publish.
  4. Poll npm view <pkg>@<version> mcpName until it answers.
  5. Install into a clean directory and run the binary.
  6. Keep description at 100 characters or fewer; run mcp-publisher validate.
  7. Confirm the domain proof is being served.
  8. mcp-publisher login and mcp-publisher publish, back to back.
  9. Query the registry and compare versions.

Try the server

The server gives an agent 14 tools for preview environments: deploy a repository and branch, wait for the live URL, read build and runtime logs, and manage environment variables. It runs without a key, so you can add it to your editor first and connect afterwards:

npx previewdrop mcp

When a tool tells you to, run npx previewdrop login. Setup for Claude Desktop, Cursor and VS Code is in the MCP docs, and a free account is at /signup.

Want previews on your own pull requests? Start free — 2 concurrent previews, no credit card.