Publishing and the MCP Registry
The one-line test
Section titled “The one-line test”A local MCP server is installed by a host config entry, not by a person running npm install. Claude Code, Claude Desktop, and most editors will happily run whatever command you give them. So the bar for “installable” is:
npx -y @acme/mcp-weather# oruvx mcp-weatherIf that command starts your server on stdio and nothing else prints to stdout, you are installable. Everything in this lesson serves that line, and then makes the line findable.
flowchart LR r["your repo"] --> p["npm or PyPI package"] p --> reg["MCP Registry: metadata only"] reg --> h["host config: claude mcp add ..."] p --> h h --> run["npx -y or uvx starts the server"]
Packaging for npm
Section titled “Packaging for npm”{ "name": "@acme/mcp-weather", "version": "1.2.0", "description": "An MCP server for weather information.", "mcpName": "io.github.acme/weather", "type": "module", "bin": { "mcp-weather": "dist/index.js" }, "files": ["dist"], "repository": { "type": "git", "url": "https://github.com/acme/mcp-weather.git" }, "engines": { "node": ">=22" }, "dependencies": { "@modelcontextprotocol/server": "^2.0.0", "zod": "^4.0.0" }}#!/usr/bin/env node// dist/index.js — the bin entryimport { McpServer } from '@modelcontextprotocol/server';import { serveStdio } from '@modelcontextprotocol/server/stdio';import * as z from 'zod/v4';
serveStdio(() => { const server = new McpServer({ name: 'weather', version: '1.2.0' }); server.registerTool( 'get-forecast', { description: 'Get the weather forecast for a city', inputSchema: z.object({ city: z.string() }) }, async ({ city }) => ({ content: [{ type: 'text', text: `Sunny in ${city} all week.` }] }), ); return server;});[project]name = "mcp-weather"version = "1.2.0"description = "An MCP server for weather information."requires-python = ">=3.11"dependencies = ["mcp>=2.0"]
[project.scripts]mcp-weather = "mcp_weather.server:main"
[build-system]requires = ["hatchling"]build-backend = "hatchling.build"from mcp.server import MCPServer
mcp = MCPServer("weather")
@mcp.tool()def get_forecast(city: str) -> str: """Get the weather forecast for a city.""" return f"Sunny in {city} all week."
def main() -> None: mcp.run() # stdio by defaultChecklist for both ecosystems:
- The entry point runs on stdio by default with no flags, so
npx -y/uvxjust works. - The shebang line and executable bit are present (
binscripts on npm;[project.scripts]on PyPI). - No stdout output except protocol messages. Startup banners go to stderr or nowhere.
- Configuration comes from environment variables (
WEATHER_API_KEY), which every host can inject, not from interactive prompts. mcpNameinpackage.jsonis the registry name you will use later; the registry verifies that the published package carries it.
Publish as usual: npm publish --access public or uv build && uv publish.
The README is the install UI
Section titled “The README is the install UI”Hosts do not read your README, but the person configuring the host does, and they will paste from it. Put a ready-to-run config block near the top for each major host:
# Claude Codeclaude mcp add --transport stdio weather \ --env WEATHER_API_KEY=your-key \ -- npx -y @acme/mcp-weather{ "mcpServers": { "weather": { "command": "npx", "args": ["-y", "@acme/mcp-weather"], "env": { "WEATHER_API_KEY": "your-key" } } }}Then list every environment variable with whether it is required and secret, the tools the server exposes (name and one line each), and the scopes or permissions a remote variant needs. A reader should be able to decide whether to install from the README alone.
The official MCP Registry
Section titled “The official MCP Registry”The registry at https://registry.modelcontextprotocol.io is a metadata catalogue, currently in preview: it stores a server.json describing where a server lives, not the artifacts themselves. Your package still lives on npm, PyPI, or a container registry. Hosts and directories query the registry to discover servers.
1. Install mcp-publisher
Section titled “1. Install mcp-publisher”brew install mcp-publisher# or the release binary: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/2. Generate and edit server.json
Section titled “2. Generate and edit server.json”mcp-publisher init{ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", "name": "io.github.acme/weather", "description": "An MCP server for weather information.", "repository": { "url": "https://github.com/acme/mcp-weather", "source": "github" }, "version": "1.2.0", "packages": [ { "registryType": "npm", "identifier": "@acme/mcp-weather", "version": "1.2.0", "transport": { "type": "stdio" }, "environmentVariables": [ { "name": "WEATHER_API_KEY", "description": "API key for the weather provider", "isRequired": true, "isSecret": true, "format": "string" } ] } ]}Rules that trip people up:
nameis reverse-DNS and must matchmcpNameinpackage.json.- With GitHub authentication the name must start with
io.github.<your-username>/. versionat the top and inside each package should track your release.registryTypecan benpm,pypi, oroci(a container image). List more than one package if you ship both an npm and a PyPI build.- Hosted servers add a
remotesarray instead of (or alongside)packages, giving the Streamable HTTP URL so hosts can connect without installing anything.
3. Log in and publish
Section titled “3. Log in and publish”mcp-publisher login github # device-code flow in the browsermcp-publisher publish4. Verify
Section titled “4. Verify”curl "https://registry.modelcontextprotocol.io/v0.1/servers?search=io.github.acme/weather"The response is {"servers":[{ ... "name":"io.github.acme/weather" ... }]}.
Common errors: Registry validation failed for package means mcpName is missing from the published package; Invalid or expired Registry JWT token means log in again; You do not have permission to publish this server means the namespace does not match your authentication method.
Custom namespaces
Section titled “Custom namespaces”GitHub auth gives you io.github.<user>/. To publish under your own domain (com.acme/weather) use DNS authentication: prove control of the domain with a TXT record, then the registry lets you publish any name under com.acme/. The registry docs also cover publishing from GitHub Actions for release automation.
Distributing inside a team
Section titled “Distributing inside a team”Not every server belongs in a public registry. For an internal tool, commit a .mcp.json at the root of the repository that needs it; Claude Code picks it up with --scope project semantics and every teammate gets the same servers on checkout:
{ "mcpServers": { "shared-server": { "type": "http", "url": "https://mcp.internal.example.com/mcp" }, "local-db": { "type": "stdio", "command": "npx", "args": ["-y", "@acme/mcp-db", "--config", "${CLAUDE_PROJECT_DIR}/db.json"], "env": { "DB_URL": "${DB_URL:-postgresql://localhost/dev}" } } }}${VAR} and ${VAR:-default} expand in command, args, env, and headers, so secrets stay in each developer’s environment while the file itself is safe to commit.