Skip to main content

How to Publish an MCP Server to the MCP Registry

Short answer

Publish your server's npm package first, then describe it in a server.json file and publish that to the official MCP Registry with mcp-publisher publish. Add an mcpName field to package.json that matches the name in server.json, and log in with GitHub so that name starts with io.github.<your-username>/. The registry stores only metadata and points to your npm package. Each release needs a new version string, because a published version can't be changed.

Tested withMCP Registry server.json schema 2025-12-11mcp-publisher 1.8.1MCP TypeScript SDK (@modelcontextprotocol/server) 2.3.1MCP Inspector 2.11.0Node.js 24.19 (Inspector needs 22.19 or later)Checked 20 Oct 2026

What you'll have at the end​

A server called unit-converter, listed in the MCP Registry as io.github.your-username/unit-converter and backed by the npm package @your-username/mcp-unit-converter. Pushing a v* tag to your repository then publishes each new version without you running the commands by hand.

Before you start

  • Node.js 22.19 or later for the MCP Inspector check. The Inspector package declares node: '>=22.19.0'.
  • An npm account. The registry hosts only metadata, so the package itself must be on npm first. For npm packages the registry supports the public npm registry only.
  • A GitHub account. GitHub login proves you own the name in server.json.
  • A server to publish. Use the example in this guide or your own npm-based server. How to build an MCP server explains how to write one.
  • The mcp-publisher CLI, installed in step 4.

1. Build the server​

The example is a small stdio server with one tool, celsius_to_fahrenheit. Create the project with the files below, or copy them from the example folder, then install and build it:

npm install
npm run build
src/index.ts
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/server";
import { serveStdio } from "@modelcontextprotocol/server/stdio";
import { z } from "zod";

// serveStdio calls this whenever it needs a server instance,
// so it must return a new McpServer every time.
function createServer() {
const server = new McpServer({ name: "unit-converter", version: "1.0.0" });

server.registerTool(
"celsius_to_fahrenheit",
{
description:
"Convert a temperature from degrees Celsius to degrees Fahrenheit. Use this when the user asks to convert Celsius to Fahrenheit.",
inputSchema: z.object({
celsius: z.number().describe("Temperature in degrees Celsius, for example 21.5"),
}),
},
async ({ celsius }) => {
const fahrenheit = (celsius * 9) / 5 + 32;
return {
content: [
{ type: "text" as const, text: `${celsius} C is ${fahrenheit} F` },
],
};
},
);

return server;
}

// Logs go to stderr. Anything written to stdout corrupts the stdio transport.
serveStdio(createServer, {
onerror: (error) => console.error("Unit converter error:", error),
});
console.error("Unit converter MCP server running on stdio");

The server logs to stderr for the same reason as the reference server: anything printed to stdout would corrupt the protocol. Check that the tool is listed before you touch the registry:

npx -y @modelcontextprotocol/inspector@2.11.0 --cli node build/index.js --protocol-era modern --method tools/list

You should see celsius_to_fahrenheit in the output. Test with MCP Inspector covers the other Inspector modes.

2. Add the registry fields to package.json​

The registry checks that the npm package really belongs to the server it describes. It does that with the mcpName field, which must match the name in server.json.

package.json
{
"name": "@your-username/mcp-unit-converter",
"version": "1.0.0",
"mcpName": "io.github.your-username/unit-converter",
"description": "An MCP server that converts temperatures from Celsius to Fahrenheit.",
"license": "MIT",
"type": "module",
"bin": {
"mcp-unit-converter": "./build/index.js"
},
"scripts": {
"build": "tsc && chmod 755 build/index.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/your-username/mcp-unit-converter.git"
},
"files": ["build"],
"dependencies": {
"@modelcontextprotocol/server": "^2.3.1",
"zod": "^4.6.7"
},
"devDependencies": {
"@types/node": "^22.0.0",
"typescript": "^7.0.2"
}
}

A few things to note in this file:

  • mcpName is the server's registry name. For GitHub login it has to start with io.github.your-username/, where your-username is your GitHub username or an organization you belong to.
  • version must match the version in server.json.
  • files limits the npm package to build/, so the published package contains only the compiled server.

Replace your-username in package.json, server.json and the repository URL with your own values. The npm package name has to sit under a scope your npm account can publish to.

3. Publish the package to npm​

Log in to npm if you haven't already, then publish with public access:

npm adduser
npm publish --access public

Visit https://www.npmjs.com/package/@your-username/mcp-unit-converter to confirm the package is there. The registry checks the npm package when you publish the server, so do this step first.

4. Install mcp-publisher​

mcp-publisher is the official command-line tool for the registry. Install it with one of these commands:

curl -L "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher && sudo mv mcp-publisher /usr/local/bin/

Or with Homebrew:

brew install mcp-publisher

Check the install with mcp-publisher --help, and check the version with mcp-publisher --version. This guide was checked against version 1.8.1. The Homebrew copy on the machine used for testing was 1.3.8, and that build doesn't list the validate or status commands, so update it if they're missing.

5. Write server.json​

server.json is the registry's description of your server: its name, what it does, where its code lives and how to run it. Save this as server.json next to package.json:

server.json
{
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
"name": "io.github.your-username/unit-converter",
"title": "Unit Converter",
"description": "Convert temperatures from Celsius to Fahrenheit.",
"repository": {
"url": "https://github.com/your-username/mcp-unit-converter",
"source": "github"
},
"version": "1.0.0",
"packages": [
{
"registryType": "npm",
"identifier": "@your-username/mcp-unit-converter",
"version": "1.0.0",
"transport": {
"type": "stdio"
}
}
]
}
  • name is a namespace and a server name separated by one slash. The namespace comes from how you log in, and the server name is yours to choose.
  • title is the display name, and description is the one-line summary. Both have a 100-character limit in the schema, and the registry rejects a longer description.
  • repository points to the source code, so people can inspect it.
  • version and the version inside packages must match the version in package.json.
  • packages describes how to run the npm package. This example uses stdio, which means a client starts it as a local process.

You don't have to write this file from scratch. Run mcp-publisher init in the project folder. It reads package.json and your git remote, then writes a template. The template includes an environmentVariables block for a YOUR_API_KEY that this server doesn't use, so delete that block before you continue.

Check the file against the registry without publishing anything:

mcp-publisher validate

The command reports that server.json is valid. If it reports an error, fix the file and run it again before you log in.

6. Log in​

Run the login command for the namespace you want:

mcp-publisher login github

The command prints a URL, https://github.com/login/device, and a one-time code. Open the URL, enter the code and authorize the app. The terminal confirms the login once GitHub accepts it.

The login method decides which names you can publish:

LoginServer name must start withExample
mcp-publisher login githubio.github.<username or org>/io.github.your-username/unit-converter
mcp-publisher login dnsYour domain in reverse order, such as com.example/com.example/weather

With DNS, you prove you control a domain by adding a TXT record to its DNS. The record looks like this, where the public key comes from a key pair you generate:

example.com. IN TXT "v=MCPv1; k=ed25519; p=<public key>"

Then run mcp-publisher login dns --domain example.com --private-key <private key>. The authentication guide has the commands to generate the key pair. The HTTP method works the same way, with a file at /.well-known/mcp-registry-auth instead of a DNS record.

warning

If you log in as a GitHub account that doesn't own the namespace in name, publishing fails with "You do not have permission to publish this server." Log in as the owner, or change the namespace to match your login.

7. Publish to the registry​

From the folder that contains server.json, run:

mcp-publisher publish

The command publishes to https://registry.modelcontextprotocol.io and reports the server name and version when it succeeds. To check that the registry has your server, search for it through the registry's API:

curl "https://registry.modelcontextprotocol.io/v0.1/servers?search=io.github.your-username/unit-converter"

The response is JSON with a servers list, and each entry carries the name you published.

8. Publish a new version​

Each release follows the same three steps:

  1. Change version to the new value in both package.json and server.json. For example, 1.0.1.
  2. Publish the new npm package with npm publish --access public.
  3. Run mcp-publisher publish again.

The registry needs every version string to be unique, and it doesn't let you change a version after you publish it. To fix the metadata of a release, publish a new version. Use semantic versioning where you can. The registry rejects version ranges such as ^1.2.3, and it sorts semantic versions to decide which one is latest.

If you only need to change registry metadata and the npm package stays the same, use a prerelease version such as 1.0.0-1 with packages still pointing at 1.0.0. Prereleases sort before the release they belong to, so a 1.0.0-1 published after 1.0.0 won't be marked latest.

To warn people about a version, change its status with a message. Put the flags before the positional arguments:

mcp-publisher status --status deprecated --message "Use 1.0.1 instead" io.github.your-username/unit-converter 1.0.0

The registry FAQ says servers can't currently be deleted, so status is the tool you have for a version that needs a warning.

9. Publish from GitHub Actions​

Once you can publish by hand, you can let a workflow do it on every version tag. Save the file below as .github/workflows/publish-mcp.yml in your repository:

publish-mcp.yml
name: Publish to MCP Registry

on:
push:
tags: ["v*"]

jobs:
publish:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read

steps:
- name: Check out code
uses: actions/checkout@v5

- name: Set up Node.js
uses: actions/setup-node@v5
with:
node-version: "lts/*"
registry-url: "https://registry.npmjs.org"

- name: Install dependencies
run: npm ci

- name: Build package
run: npm run build --if-present

- name: Check tag matches package version
run: test "${GITHUB_REF#refs/tags/}" = "v$(node -p "require('./package.json').version")"

- name: Publish package to npm
run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Install mcp-publisher
run: |
curl -L "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher

- name: Authenticate to MCP Registry
run: ./mcp-publisher login github-oidc

- name: Publish server to MCP Registry
run: ./mcp-publisher publish

What the workflow does, in order:

  • It runs on any tag that starts with v, such as v1.0.1.
  • It publishes the npm package. This needs an NPM_TOKEN secret in your repository settings, holding an npm token that can publish the package.
  • It fails early if the tag doesn't match the version in package.json, so you can't publish v1.0.2 from a 1.0.1 package.
  • It signs in to the registry with mcp-publisher login github-oidc. The id-token: write permission lets GitHub issue the token, so you don't need a registry secret. This login works for repositories under the GitHub account or organization in your server's namespace.
  • It publishes the server with mcp-publisher publish.

The registry-url setting on actions/setup-node makes it write an npm config that reads the NODE_AUTH_TOKEN variable, which is how the NPM_TOKEN secret reaches npm publish. The GitHub Actions guide also documents a personal access token login, mcp-publisher login github --token, which needs a token with the read:org and read:user scopes stored in a repository secret.

Release a version by pushing its tag:

git tag v1.0.0
git push origin v1.0.0

Other package types​

This example uses npm. The registry supports other package types too, and each has its own way to prove you own the package. Check the one you use before you publish:

registryTypeWhere the registry looks for the ownership marker
npmmcpName in package.json
pypiAn mcp-name: <server name> line in the package README, which may sit in an HTML comment
nugetThe same mcp-name: line in the package README
cargoA visible mcp-name: line in the README. crates.io strips HTML comments, so a hidden comment doesn't work
ociAn io.modelcontextprotocol.server.name label on the image, such as LABEL io.modelcontextprotocol.server.name="io.github.your-username/..." in a Dockerfile
mcpbA .mcpb file hosted on GitHub or GitLab releases, with a fileSha256 value and mcp in the URL. See how to package an MCP server as an .mcpb file

A server that runs at a public URL doesn't need a package at all. Put a remotes entry in server.json in place of packages, with a type of streamable-http and a public url. The registry checks that you own the name, not the URL, so the endpoint must already be live. Deploy a remote MCP server covers hosting one.

Also list it in the MCPBundles directory​

A remote server with a public HTTPS URL can also be listed in the MCPBundles directory at www.mcpbundles.com. Publishing there is separate from the MCP Registry: you give it the server URL, a name and a description, and it doesn't change your server.json or npm package. Publish an MCP server to MCPBundles walks through the steps.

Common problems​

Registry validation failed for package The registry couldn't find the ownership marker for your package. For npm, mcpName is missing from package.json or doesn't equal the name in server.json. Fix the field and publish again. If that version is already on npm, bump the version first, because a published version can't be changed.

Invalid or expired Registry JWT token Your saved login has expired or is no longer valid. Run mcp-publisher login github again.

You do not have permission to publish this server The namespace in name doesn't match the account you logged in with. With GitHub login the name has to start with io.github.<your-username>/, where the username is the account you logged in as.

Error: not authenticated, run 'mcp-publisher login <method>' first You haven't logged in on this machine, or the login was saved by an older version. The mcp-publisher 1.8.1 build prints hint: token storage moved to ~/.config/mcp-publisher/. If you recently upgraded, please re-login. in that case. Run the login command again.

FAQ​

Do I need to host my server to publish it to the MCP Registry?

No, not for a local server. The registry hosts only metadata, so the code stays in its package registry, such as npm, and the registry entry points to it. A remote server does need a public URL that is reachable from the internet.

Can I delete a server from the MCP Registry?

Not at the moment. The registry FAQ says servers can't currently be deleted, and there is an open discussion about it. To warn people about a version, use mcp-publisher status to mark it deprecated or deleted, with a message.

Can I edit a version after I publish it?

No. Once a version is published, its version string and other metadata can't be changed. Publish a new version with a new version string instead.

Do I have to publish the npm package before the MCP Registry?

Yes, for an npm server. The registry checks the npm package when you publish the server, so the package has to exist on npm at the version named in server.json first.

Can I use the same server name on the MCP Registry and on npm?

They are different things. The npm package name is the package's install name, such as @your-username/mcp-unit-converter. The registry name, such as io.github.your-username/unit-converter, is the identifier the registry uses, and mcpName in package.json has to match it.