Packaging and Distributing
Your server works on your machine. You run it from a checkout, point your host config at an absolute path, and it connects. Now someone else wants it, and “clone the repo, install the deps, edit this JSON to match your paths” is not an install story anyone will follow.
A server nobody can install in one step is a server nobody uses. This article is about the last mile: turning a working script into a package a stranger installs with one command, documents so they can wire it up, lists so they can find it, and (if it needs to be) hosts remotely.
Package so one command runs it
The bar to clear: a user pastes a config block into their host, restarts it, and your server runs. No clone, no manual dependency install, no path editing. Two package ecosystems get you there.
- npm, run through
npx. The host config invokesnpx your-server; the runner downloads and runs it in one step. - PyPI, run through
uvx(from uv) orpipx. Same idea for Python:uvx your-serverfetches and runs without touching the user’s environment.
The trick that makes both work is a binary entry point: a name the runner can execute directly. You declare it in your package metadata and point it at your server’s main function.
For a Node server, that is the bin field in package.json:
{
"name": "mcp-server-weather",
"version": "0.1.0",
"bin": { "mcp-server-weather": "dist/index.js" },
"files": ["dist"],
"dependencies": { "@modelcontextprotocol/sdk": "^1.0.0" }
}
The first line of dist/index.js needs the shebang #!/usr/bin/env node so the shell knows how to run it. Now npx mcp-server-weather works for anyone.
For a Python server, the equivalent lives in pyproject.toml:
[project]
name = "mcp-server-weather"
version = "0.1.0"
dependencies = ["mcp>=1.0.0"]
[project.scripts]
mcp-server-weather = "mcp_server_weather:main"
main is a plain function that starts your server. After you publish, uvx mcp-server-weather fetches from PyPI and runs it. Pick one ecosystem, match it to the language your server is written in, and do not overthink it.
Why is npx/uvx better than "clone and run"?
Two reasons: isolation and updates.
npx and uvx install your package into a throwaway cache, not the user’s project or global environment. There is nothing to uninstall and no version of your dependency fighting with theirs. A clone-and-run setup leaks its dependencies into whatever the user already has.
The second reason is updates. When you publish a new version, the runner picks it up on the next launch (or on a pinned version bump). A cloned repo sits at whatever commit the user pulled and quietly rots. The whole point of a package is that the version is a number, not a git SHA.
Version it, and write the README that gets pasted
Versioning is not paperwork. Hosts and registries key off your version number, and users pin to it. Use semantic versioning: bump the patch digit for fixes, the minor for new tools, the major when you rename or remove a tool and break callers. Renaming a tool is a breaking change even though nothing crashes, because an agent’s saved prompts and a user’s muscle memory both point at the old name.
The README does more work than the code for adoption, because it is what a new user reads first. Three things have to be there:
-
The config block, ready to paste. This is the JSON the user drops into their host. Getting a host to see a server is covered in article 07: Connecting a Server to Hosts; your job here is to hand the user the exact block so they do not have to reverse-engineer it.
{ "mcpServers": { "weather": { "command": "npx", "args": ["-y", "mcp-server-weather"], "env": { "WEATHER_API_KEY": "your-key-here" } } } } -
Required environment variables, named and explained. Every secret or setting your server reads, what it is for, and where to get it.
WEATHER_API_KEY: your key from example.com/account. Never bury a required variable in the code and let the user discover it from a crash. -
One worked example. A single sentence a user can type into the host and the tool call it triggers. “Ask: what is the forecast for Amsterdam? and the model calls
get_forecast.” This proves the wiring end to end and shows what the server is for in one line.
Keep the tool descriptions themselves tight and honest, which the quality guidance in article 06: Server Best Practices goes into. The README sells the server; the tool descriptions are what the model actually reads.
Registries and discovery
A published package that nobody can find is a private package. Two kinds of listing fix that.
The official MCP registry is the canonical index. It is a metadata catalog: you publish a server.json describing your server (name, version, install command, transport), and other tools sync from it. It is the source of truth, the way npm’s registry underpins the Node ecosystem.
Directories like Smithery are the human-facing front end: searchable listings, categories, sometimes one-click install into a specific host. They are how a person browsing for “a server that talks to Postgres” finds yours.
flowchart LR
DEV["You publish"] --> PKG["npm or PyPI<br/>(the code)"]
DEV --> REG["MCP registry<br/>(server.json)"]
REG --> DIR["Directories<br/>e.g. Smithery"]
DIR --> USER["User finds<br/>and installs"]
PKG --> USER
The order matters. Publish the package first so there is something to install, then register the metadata that points at it, then let directories pick it up. Fill in every field the listing offers: a clear one-line description, the categories, and the required env vars. A listing with an empty description gets scrolled past.
Distributing a remote server
Everything above assumes a local server the host launches as a subprocess over stdio. That is the right default, and the reasons why are in article 02: MCP Architecture. Some servers cannot be local: a shared internal tool, a hosted product, anything where one running instance serves many users. Those need the Streamable HTTP transport, and the shape of the work changes.
flowchart LR
subgraph LOCAL ["Local (stdio)"]
H1["Host"] -->|"spawns subprocess"| S1["Your server"]
end
subgraph REMOTE ["Remote (HTTP)"]
H2["Host"] -->|"HTTPS + token"| S2["Your hosted server"]
end
Three things you did not have to think about locally now become your problem:
- Auth. A stdio server inherits the trust of the machine it runs on. A remote server is reachable over the network, so you must authenticate every request with OAuth or bearer tokens. You are now responsible for not leaking whatever the server can reach. Do not ship an open endpoint.
- Deployment and uptime. A local server exists only while the host runs it. A remote server has to stay up: a host process, a health check, readable logs, and a plan for restarts. It is a service now.
- Multi-tenancy. One process serves many users. Per-user state, per-user secrets, and rate limits are all on you. A bug that mixes one user’s data into another’s session is the kind of incident that ends a server’s reputation.
The honest version: shipping a remote server is shipping a web service. If you have run a REST API in production, you already know this job. Reach for remote only when local genuinely cannot work, because it triples the operational surface.
Common beginner mistakes
- No binary entry point: without a
binor[project.scripts]line,npx/uvxhave nothing to run and the config block fails silently. - Hardcoded absolute paths: a path that works on your machine breaks on everyone else’s. Read config from env vars.
- Undocumented env vars: a required key the user only learns about from a stack trace. List every one in the README.
- Publishing before testing the package: run
npx your-serverfrom a clean directory before you publish, not justnode index.jsfrom your checkout. - Renaming a tool in a minor version: renames break callers. That is a major bump.
- Shipping a remote server with no auth: an open network endpoint that can touch real data is an incident waiting to happen.
Questions you will face in production
“npm or PyPI, which do I publish to?”
Whichever your server is written in. A Node server goes to npm and runs via npx; a Python server goes to PyPI and runs via uvx or pipx. There is no benefit to publishing to both, and doing so just doubles your release work. Users have both runners available; they do not care which language you chose.
“Do I have to register with the official registry, or is publishing the package enough?” Publishing the package is what makes it installable; registering is what makes it discoverable. If you are only sharing the install command with a few people directly, the package alone is fine. If you want strangers to find it, register the metadata and get listed in a directory, or your server stays invisible.
“How do I handle secrets for a remote server?” Never in the URL, never in the config block a user pastes. Use a real auth flow (OAuth or issued tokens) and store per-user credentials server-side. The local pattern of “put the API key in an env var” does not transfer, because on a remote server that key would be yours and shared across every user, not theirs.
What to remember
- The install bar is one pasted config block and a restart, nothing more
- A binary entry point (
bininpackage.json,[project.scripts]inpyproject.toml) is what makesnpx/uvxwork - Semantic versioning, and remember a tool rename is a breaking change
- The README must carry the paste-ready config block, every required env var, and one worked example
- Publish the package, register the metadata, then get listed in a directory; each step does a different job
- A remote server is a web service: auth, uptime, and multi-tenancy are now yours
What to study next
You can now ship a server people can install and find. The next question is whether anyone should pay for it, and how that changes what you build and how you position it. That is where article 09: Selling Your MCP Server goes. If you have not nailed the tool quality that makes a server worth installing at all, loop back to article 06: Server Best Practices first, because polish is what turns a listing into an install.
Further reading
- Model Context Protocol: Server registry. The official docs, including how the registry and
server.jsonmetadata work. - Smithery. A widely used directory for discovering and installing MCP servers.
- npm docs: bin field. How the executable entry point that
npxruns is declared. - uv: tools and uvx. Running Python packages as commands without a manual install.
Where this article comes from. This is a synthesis of the MCP specification and common packaging practice as of 2026, not a citation of any single paper. The sources above are where the mechanics come from. If you find an error or have a better source for a claim, the article gets fixed within a day, send me a note.