Publish an MCP server
This skill walks through releasing an MCP server to npm, the official MCP Registry, and Smithery. It does not assume any particular repo layout — determine the actual setup first, then follow the release steps against that reality.
This file holds the flow and the stop-gates. Detailed rationale, full
error tables, and one-time-setup specifics live in references/ — load
the relevant file when a check fails or you need the "why," not up front.
Step 0 — Determine the actual setup (never assume)
Before touching anything, establish facts about this project instead of guessing. Where a check below only yields a heuristic rather than a guarantee, that's called out explicitly — treat those as evidence to weigh, not as settled conclusions.
- Where does
npm publishactually run from? Find the package'spackage.jsonand check itsrepository.url. Compare that togit remote -vin the current checkout.- Heuristic, not proof: a match (or no
repositoryfield) is evidence — not confirmation — of a single-repo setup; a mismatch is evidence, not proof, of a mirror/sync setup (the field could just be stale or wrong). If the result is a mismatch, or anything else about the setup is unclear, stop here and ask the user to confirm the actual release flow before proceeding to Step 1 — don't invent a mirror-sync step that doesn't exist, and don't skip one that does. Only proceed without asking when the match is clean and step 0.2/0.3 below don't surface anything contradicting it.
- Heuristic, not proof: a match (or no
- Is there existing release documentation? Check the package's own
README.md, aCONTRIBUTING.md, or adocs/folder for anything describing how past releases were done. Trust that over any assumption. - Has this package been published before? Run
npm view <package-name> version(if the name is already known) to see whether this is a first publish or a version bump, and whether the localversioninpackage.jsonis ahead of what's live. - What transport does the server use? Local stdio (spawned via
npx/node, no public URL) or a hosted remote HTTP endpoint? Check the server's own README/config for how clients actually connect to it. This decides which Smithery path applies in Step 4. If it's not obvious, this is one of the things to ask about in the single gate below — don't guess it from what's common in older docs. - What semver level does this change need? Patch/minor/major, per
semver applied to the actual change — not a default:
- Patch (
x.y.Z) — bug fixes, docs, internal refactors; no change to the tool/API surface a client would notice. - Minor (
x.Y.z) — new tools, new optional parameters, new capabilities — backwards-compatible additions. - Major (
X.y.z) — anything that breaks an existing client: a removed/renamed tool, a required-parameter change, a changed response shape, a changed transport/auth requirement. If the user has already said what kind of change this is ("just a bugfix", "I added a new tool"), map that to the level above. If it isn't obvious from the actual diff, this is one of the things to confirm in the gate below — don't guess silently, since picking the wrong level here is exactly the kind of inconsistency this skill exists to prevent.
- Patch (
Only proceed to the steps below once you know: which directory to publish from, whether any sync/build step precedes it, what the current live version is, the server's transport, and the semver level for this release.
Stop here — one consolidated round of questions, then one explicit go-ahead, before touching Steps 2–5. Ask all of this together, don't re-ask it piecemeal as you reach each step:
- Scope — which destinations does this release cover? npm (Step 2) is assumed; confirm whether the MCP Registry (Step 3), Smithery (Step 4), and/or the one-time "other listings" (Step 5) are in scope for this release. Treat silence on Registry/Smithery/Step 5 as no, not as implied yes.
- Semver level (only if step 0.5 above didn't already settle it) — patch, minor, or major?
- MCP Registry login method (only if Registry is in scope) — GitHub
is the default
mcp-publisherlogin this skill assumes; confirm that's right if it's not already obvious. - Server transport (only if Smithery is in scope and step 0.4 above didn't already settle it) — remote HTTP URL, or local stdio needing MCPB bundling?
- Go-ahead — explicit "yes, publish this version" for this specific release. These are public, hard-to-reverse actions (npm blocks unpublishing a version after 72 hours; MCP Registry/Smithery listings have no clean delete), so a general "publish this" request earlier in the conversation doesn't count as this go-ahead — get it here, for this release, then proceed without asking again per-destination.
What this gate does not cover: the mechanical waits inside Steps 2–4 (completing a browser/device-code login, entering a 2FA code). Those are physical actions the user performs in the moment the CLI actually prompts for them, not decisions — each step still pauses in place for those regardless of what's answered here.
Step 1 — Bump the version, commit, and tag
Using the semver level determined in Step 0 (and confirmed in the gate, if it wasn't obvious):
If the package does not ship a server.json, npm version does the
whole job in one command — bump, commit, and tag together (requires a
clean working tree, which it enforces itself):
npm version <patch|minor|major> # bumps package.json, commits "vX.Y.Z", tags vX.Y.Z
If the package does ship a server.json (used by the official MCP
Registry), npm version's auto-commit won't pick that file up, and it
refuses to run at all against a dirty tree — so do it manually instead of
fighting the tool. Regenerate the lockfile too, not just package.json —
a hand-edited version bump leaves package-lock.json's own version field
stale unless something re-syncs it:
# bump `version` in package.json AND server.json's version field
# (and the matching packages[].version field) to the same value —
# the registry rejects a server.json whose version doesn't match npm
npm install --package-lock-only # syncs package-lock.json's version to match
git add package.json package-lock.json server.json
git commit -m "vX.Y.Z"
git tag "vX.Y.Z"
Either way (whichever path above applies), push the commit now —
that's cheap to correct later if something's off. Don't push the tag
yet, even though npm version already created it locally:
git push
Hold git push --tags (or git push origin "vX.Y.Z") until Step 2 below
confirms the npm publish actually succeeded. A tag that's already been
pushed for a version that never made it to npm is a mess to clean up —
deleting a pushed tag is a disruptive, hard-to-reverse action in its own
right — so the fix is simply not pushing it until it's earned. The tag
push happens at the end of Step 2's verify block.
Out of scope, on purpose: this skill creates a git tag but not a
GitHub Release. If your workflow wants one, run something like
gh release create vX.Y.Z --generate-notes yourself afterward — release
notes are a separate, more editorial step this skill won't presume to
write for you.
If step 0 revealed a sync step (e.g. a monorepo package copied into a
separate release repo), do the sync — and the commit+tag+commit-push
above — in the release repo (the one Step 0 established as where
npm publish actually runs from), not the monorepo. Build first, then
copy/commit/push. Don't invent file lists; copy whatever step 0 showed
you actually gets synced.
Step 2 — Publish to npm
Pre-flight checks — run before the auth check below, and before
npm publish itself (see references/npm-publish.md for why each one
matters and what to do if one fails):
git status # working tree should be clean
npm run build # if the package defines a build script — must succeed
npm test # if the package defines a test script — must pass
npm run lint # if the package defines a lint script — must pass
npm pack --dry-run # preview exactly what would be published, without publishing
If any of these are dirty, fail, or look wrong in the file list, stop
— see references/npm-publish.md for what each failure means.
Only move on once all of the above pass. npm publish requires an
authenticated npm session:
npm whoami # errors if not logged in
If that errors, stop here and tell the user to log in or configure a
token — don't authenticate on their behalf (references/npm-publish.md
has the exact commands). Wait for them to confirm before continuing.
The pre-flight npm run build above already produced the build this
publishes — don't build again here. From the directory established in
step 0:
npm publish
Verify — don't assume exit 0 means it's live:
npm view <package-name> version
Confirm this returns the version you just bumped to. Only report this
step done once it matches. If npm publish failed instead, see
references/npm-publish.md for the error table (403 duplicate version,
402 payment required, EOTP, collaborator/403, 401 — each has a distinct,
non-interchangeable recovery).
Now — and only now — push the tag from Step 1:
git push origin "vX.Y.Z"
This is the point the tag stops being provisional. If npm publish
failed above, don't push the tag — fix the failure, and if the fix
changes the version number, the old local tag is now wrong too (delete
and recreate it locally, at the new version, before eventually pushing).
Step 3 — Publish to the official MCP Registry
(Skip this step entirely if Registry wasn't in scope per the Step 0 gate.)
One-time setup: install the mcp-publisher CLI, then log in with the
method confirmed in the Step 0 gate (GitHub is the default). Login blocks
on a browser approval — run the command, then wait for the user to
confirm they've completed it before continuing (see
references/mcp-registry.md for login-method and JWT-lifetime detail):
mcp-publisher login github # device-code flow
Then, from the directory containing server.json, run the dry run — but
don't chain straight into the real publish after it:
mcp-publisher publish --dry-run
Observed, not a documented guarantee: --dry-run has been seen to
not be a true no-op on at least one CLI version — it can actually
submit the release. So check before deciding whether to run the plain
publish:
curl -s "https://registry.modelcontextprotocol.io/v0/servers?search=<name>"
- If the version you just bumped to is already listed with
status: active, the dry-run submitted it — you're done. Do not runmcp-publisher publish; it will 400 with "cannot publish duplicate version." - If it's not listed yet, the dry-run only validated. Proceed:
mcp-publisher publish
Then re-run the same curl check to confirm the real publish landed —
don't report this step done on the CLI's exit code alone. If it failed
instead, see references/mcp-registry.md for the error table (400
duplicate version, 401 expired token, schema validation errors, 403
naming/ownership).
Documented registry rule (fact, not heuristic): if auth is via
GitHub, server.json's name field must be namespaced
io.github.<github-username>/... (or a verified org equivalent).
Step 4 — Publish/update on Smithery
(Skip this step entirely if Smithery wasn't in scope per the Step 0 gate.)
Smithery's mcp publish command only accepts a URL (hosted/remote
servers) or an MCPB bundle — never a raw npx command. Use the
transport confirmed in the Step 0 gate: if it's a hosted remote HTTP
endpoint, publish that URL directly and skip bundling entirely —
smithery mcp publish https://your-server-url
Otherwise (local stdio): stop and load references/smithery.md in full
before writing manifest.json or running anything below. It's not
optional background reading — it lists the required manifest.json
fields (user_config entries, sensitive flags, entry-point wiring), and
a bundle built without them fails mcpb validate or, worse, silently
ships without required config. Once you've read it and built
manifest.json accordingly:
npx @anthropic-ai/mcpb validate manifest.json
npx @anthropic-ai/mcpb pack . <package-name>.mcpb
smithery auth login
smithery mcp publish <package-name>.mcpb -n <namespace>/<package-name>
smithery auth login opens a browser-based login and blocks until the
user completes it — wait for their confirmation before running publish.
Note the printed "Namespace" (org-scoped); publish needs
<namespace>/<name>, not just the package name.
Verify — don't assume the CLI's exit code is the last word:
curl -s "https://registry.smithery.ai/servers/<namespace>/<name>"
smithery mcp search specifically lags — returning nothing there is
not evidence of failure, don't use it as the check. If publish or
bundling failed instead, see references/smithery.md for the error table
and the republish procedure for future versions.
Step 5 — Other listings (one-time, not per-release, and optional)
(Skip this step entirely unless "other listings" was confirmed in scope
per the Step 0 gate — they're not part of a routine release.) See
references/other-listings.md for PulseMCP, Glama, mcp.so, and
awesome-mcp-servers submission details.
Checklist
- Determine setup facts (step 0): publish location/sync, live version, transport, semver level — don't guess.
- Consolidated gate (end of step 0): scope, semver level (if not already settled), registry login method, transport (if not already settled), and go-ahead — asked once, together.
- Bump
versionusing the semver level from the gate (npm version, or manual bump +npm install --package-lock-only+ commit + tag ifserver.jsonis present), sync if step 0 showed a sync step exists. Push the commit now; hold the tag push until publish is verified. (A GitHub Release is a deliberate non-goal — do that separately if wanted.) - Pre-flight checks (build happens here, once), then
npm publish(no rebuild), then verify withnpm view <package-name> version— only once that's confirmed, push the tag. Don't push a tag for a version that didn't actually make it to npm. - If Registry was in scope: dry-run, check the registry API before
deciding whether to run the plain
publish, then verify again after. - If Smithery was in scope and it's local stdio: load
references/smithery.mdbefore writingmanifest.json— not optional. Then URL-publish (remote) or MCPB bundle+publish (local), and verify via the registry API/releases page — notsmithery mcp search, which lags. - If "other listings" was in scope: work through step 5's list. Otherwise stop here.