How to Publish an MCP Server to the MCP Registry
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.
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-publisherCLI, 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
#!/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.
{
"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:
mcpNameis the server's registry name. For GitHub login it has to start withio.github.your-username/, whereyour-usernameis your GitHub username or an organization you belong to.versionmust match theversioninserver.json.fileslimits the npm package tobuild/, 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:
- macOS
- Linux
- Windows
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
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/
$arch = if ([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture -eq "Arm64") { "arm64" } else { "amd64" }; Invoke-WebRequest -Uri "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_windows_$arch.tar.gz" -OutFile "mcp-publisher.tar.gz"; tar xf mcp-publisher.tar.gz mcp-publisher.exe; rm mcp-publisher.tar.gz
Move mcp-publisher.exe to a folder on your PATH.
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:
{
"$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"
}
}
]
}
nameis 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.titleis the display name, anddescriptionis the one-line summary. Both have a 100-character limit in the schema, and the registry rejects a longerdescription.repositorypoints to the source code, so people can inspect it.versionand theversioninsidepackagesmust match theversioninpackage.json.packagesdescribes how to run the npm package. This example usesstdio, 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:
| Login | Server name must start with | Example |
|---|---|---|
mcp-publisher login github | io.github.<username or org>/ | io.github.your-username/unit-converter |
mcp-publisher login dns | Your 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.
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:
- Change
versionto the new value in bothpackage.jsonandserver.json. For example,1.0.1. - Publish the new npm package with
npm publish --access public. - Run
mcp-publisher publishagain.
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:
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 asv1.0.1. - It publishes the npm package. This needs an
NPM_TOKENsecret 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 publishv1.0.2from a1.0.1package. - It signs in to the registry with
mcp-publisher login github-oidc. Theid-token: writepermission 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:
registryType | Where the registry looks for the ownership marker |
|---|---|
npm | mcpName in package.json |
pypi | An mcp-name: <server name> line in the package README, which may sit in an HTML comment |
nuget | The same mcp-name: line in the package README |
cargo | A visible mcp-name: line in the README. crates.io strips HTML comments, so a hidden comment doesn't work |
oci | An io.modelcontextprotocol.server.name label on the image, such as LABEL io.modelcontextprotocol.server.name="io.github.your-username/..." in a Dockerfile |
mcpb | A .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.
- The MCP RegistryWhat the registry is, how server.json works and how clients use it.
- List your server in directoriesWrite the README, tool descriptions and metadata that make a listing trustworthy.
- Publish to MCPBundlesList a remote MCP server in the MCPBundles directory and connect it from chat.
- Package an MCP server as .mcpbShip a server as a single file that people can install in Claude Desktop.
- Deploy a remote MCP serverHost a Streamable HTTP server at a public URL so you can publish it as a remote.