Skip to content

๐Ÿš€ Publishing an MCP Server to npm + the MCP Registry

This guide walks the full publish path for a stdio MCP server, using @nugehs/repoctx as the worked example. Follow it once and your server becomes installable by anyone via npm install -g, discoverable by any registry-aware MCP host (Claude Desktop, Claude Code, Codex CLI, Cursor, Goose), and verifiably owned by you.

The guide is written in the order you should execute it. Every step lists exactly one decision or command. Three real publish errors are documented inline so you skip the round-trips we hit.


โœ… Prerequisites


1. ๐ŸŽฏ Decide your scope

The npm registry has a global namespace. Many sensible names are taken. Two options:

Option Format When to use
Bare name repoctx Only if npm view <name> returns 404
Scoped name @yourorg/repoctx Recommended โ€” free, immediate, brand-consistent with your GitHub org

Check availability before deciding:

npm view repoctx version          # 404 = free; any version number = taken
npm view @yourorg/repoctx version

For this guide we use @nugehs/repoctx.


2. ๐Ÿ“‹ Prepare package.json for publishing

Add or update these fields:

{
  "name": "@nugehs/repoctx",
  "version": "1.0.0",
  "mcpName": "io.github.nugehs/repoctx",
  "bin": {
    "repoctx": "src/cli.js"
  },
  "files": [
    "src",
    "scripts",
    "README.md",
    "LICENSE",
    "CHANGELOG.md"
  ],
  "publishConfig": {
    "access": "public"
  },
  "engines": {
    "node": ">=18.18"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/nugehs/repoctx.git"
  },
  "homepage": "https://nugehs.github.io/repoctx/",
  "bugs": {
    "url": "https://github.com/nugehs/repoctx/issues"
  },
  "keywords": ["mcp", "mcp-server", "cli", "developer-tools"]
}

Don't forget mcpName and publishConfig.access

Scoped packages publish private by default. Without publishConfig.access: "public", the publish will succeed but no one can install your package without an npm paid plan. And without mcpName, the MCP Registry publish will fail later with HTTP 400.


3. ๐Ÿงช Run a dry-run

Before authenticating to npm, see exactly what would ship:

npm publish --dry-run

Eyeball the file list. Your tarball should contain only what's in the files array. If you see tests/, node_modules/, .dev-context/, or coverage/ listed, stop and tighten your files array.

A healthy CLI tarball is typically 50โ€“200 kB.


4. ๐Ÿ” Authenticate to npm

npm login

For a brand-new scope you'll also need to create the matching organization at https://www.npmjs.com/org/create:

  • Name must exactly match the scope in package.json (lowercase)
  • Choose the Free tier (unlimited public packages, $0/month)

Common error: 404 Scope not found

If you skip the org creation step, npm publish fails with

404 Not Found - PUT https://registry.npmjs.org/@yourorg%2fyourpackage
Scope not found
Create the org at the URL above, then re-run npm publish. No code changes needed.


5. ๐Ÿ“ฆ First npm publish

npm publish

If 2FA is on your account, npm prompts for an OTP. After success you'll see:

+ @nugehs/repoctx@1.0.0

Verify immediately:

npm view @nugehs/repoctx version       # โ†’ 1.0.0
npx -y @nugehs/repoctx --help          # downloads fresh, runs the CLI

npm versions are immutable

You cannot republish 1.0.0 after it's live. If you spot a typo, you ship 1.0.1. You have a 72-hour window to npm unpublish a version; after that it's permanent. So always npm publish --dry-run immediately before the real publish.


6. ๐Ÿงฐ Install the MCP Publisher CLI

The MCP Registry uses its own publishing tool that handles GitHub OAuth and manifest validation.

brew install mcp-publisher

Or download a binary release from https://github.com/modelcontextprotocol/registry/releases.


7. ๐Ÿ“ Write server.json

The MCP Registry reads a manifest file named server.json at the root of your repo (next to package.json). Minimal valid example:

{
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
  "name": "io.github.nugehs/repoctx",
  "title": "repoctx",
  "description": "Local-first code context, impact analysis, and merge-readiness verdicts for AI agents.",
  "websiteUrl": "https://nugehs.github.io/repoctx/",
  "repository": {
    "url": "https://github.com/nugehs/repoctx",
    "source": "github",
    "id": "1242199320"
  },
  "version": "1.0.0",
  "packages": [
    {
      "registryType": "npm",
      "identifier": "@nugehs/repoctx",
      "version": "1.0.0",
      "transport": {
        "type": "stdio"
      },
      "packageArguments": [
        {
          "type": "positional",
          "value": "mcp"
        }
      ]
    }
  ]
}

Field reference

Field Rule Notes
name Must match ^[a-zA-Z0-9.-]+/[a-zA-Z0-9._-]+$, max 200 chars For GitHub auth, the prefix MUST be io.github.<your-username>/
title 1โ€“100 chars Human-readable display name shown in registry list views
description 1โ€“100 chars โš ๏ธ Hard cap. Trim early.
websiteUrl URI Canonical homepage โ€” your docs site, not the GitHub repo
repository.id string GitHub repo numeric ID. Get it with gh api repos/<owner>/<repo> --jq '.id'. Prevents namespace-resurrection attacks.
version SemVer, not "latest", not a range Must match packages[0].version
packages[0].registryType npm / pypi / oci / nuget / mcpb
packages[0].identifier npm package name Must match what you published in step 5
packages[0].transport.type stdio / streamable-http / sse stdio for CLI-based MCP servers
packages[0].packageArguments array of arg specs Required if your server entry point needs subcommand args. Each entry is {"type":"positional","value":"<arg>"} or {"type":"positional","valueHint":"<hint>"}

Common error: description too long

First publish attempt for @nugehs/repoctx failed with:

HTTP 422 Unprocessable Entity
{"message":"expected length <= 100","location":"body.description"}
The original description was 272 characters. Registry list views render descriptions inline; the 100-char cap is enforced.


8. ๐Ÿ” Authenticate mcp-publisher to GitHub

mcp-publisher login github

Device-code OAuth flow opens your browser. The GitHub account you log in with must own the <your-username> namespace in server.json.name. For io.github.nugehs/repoctx the GitHub account must be nugehs.


9. ๐Ÿš€ First registry publish

mcp-publisher publish

The CLI reads server.json from the current directory and submits it.

Common error: mcpName field missing

Most common first-publish failure:

HTTP 400 Bad Request
NPM package '@nugehs/repoctx' is missing required 'mcpName' field.
Add this to your package.json: "mcpName": "io.github.nugehs/repoctx"
This is the registry's ownership-proof check: it downloads the package.json from the published npm tarball and looks for mcpName matching the registry name. If it's missing, the publish is rejected.

Fix: add mcpName to package.json, bump the version (because npm tarballs are immutable, you cannot retro-fit mcpName into 1.0.0), npm publish the new version, update server.json.version + packages[0].version to match, then re-run mcp-publisher publish.

Success looks like:

Publishing to https://registry.modelcontextprotocol.io...
โœ“ Successfully published
โœ“ Server io.github.nugehs/repoctx version 1.0.1

10. โœ… Verify the listing

Two ways to check:

# Curl the registry API directly
curl https://registry.modelcontextprotocol.io/v0/servers \
  | jq '.servers[] | select(.name == "io.github.yourname/yourserver")'

Or open the search UI: https://registry.modelcontextprotocol.io/?q=yourname

Note: the search index has a short caching delay. The publish itself is committed the moment mcp-publisher reports success; the search UI may take a minute or two to refresh.


๐Ÿ” Publishing updates

For every future version:

  1. Bump package.json and package-lock.json versions
  2. Bump server.json.version and packages[0].version to match
  3. npm run ci (or your equivalent gate)
  4. npm publish
  5. mcp-publisher publish

Tag the release on GitHub afterwards:

git tag v1.0.1
git push origin v1.0.1
gh release create v1.0.1 --generate-notes

๐Ÿ› Troubleshooting summary

Error Cause Fix
404 Scope not found (npm) npm org/scope doesn't exist yet Create org at https://npmjs.com/org/create
403 You cannot publish over the previously published versions (npm) Trying to republish an immutable version Bump version in package.json
402 Payment Required (npm) Scoped package without publishConfig.access: "public" Add the field; or once-off npm publish --access public
HTTP 422 expected length <= 100 (registry) server.json.description too long Trim to โ‰ค100 chars
HTTP 400 mcpName field missing (registry) Published npm tarball lacks mcpName Add to package.json, version-bump, npm publish, retry registry publish
404 Repository not found (registry) repository.url doesn't exist or is private Make repo public; verify URL
GitHub OAuth fails Authenticated user doesn't own the io.github.<user>/ namespace Log in as the correct GitHub account

๐ŸŽ What "being on the MCP Registry" actually buys you

A discovery channel โ€” nothing automatic beyond that:

  • โœ… Registry-aware MCP hosts (Claude Desktop, Codex CLI, Cursor, Gooseโ€ฆ) can find and install your server with one click
  • โœ… Third-party catalogs (Glama, MCP.so, awesome lists) often syndicate from the registry
  • โœ… Canonical name (io.github.<you>/<server>) survives package renames
  • โœ… Verified ownership reduces typosquatting risk
  • โŒ Does not auto-install for anyone, endorse the server, or feature it anywhere

The publishing work is the on-ramp. The adoption work is downstream of the server actually being useful.